feat(metadata): add community rating and rating count

Users rely on community feedback when evaluating books in their library. This change
introduces aggregated community ratings and rating counts from external metadata
providers, allowing users to see popular opinion directly within the book details
without needing to leave the application.

Closes #414
This commit is contained in:
neon
2026-06-25 21:49:24 +00:00
parent 9b946fc83e
commit 58031c1c83
76 changed files with 12856 additions and 46 deletions
+3 -1
View File
@@ -1,4 +1,4 @@
import type { MetadataFetchDiagnostics, MetadataProviderKey, MetadataSeriesMembership } from "./metadata-fetch";
import type { BookCommunityRating, MetadataFetchDiagnostics, MetadataProviderKey, MetadataSeriesMembership } from "./metadata-fetch";
import type { BookMetadataLockField } from "./metadata-lock";
import type { AudiobookChapter, NarratorRef } from "./audiobook";
import type { ComicMetadataFields } from "./metadata-fetch";
@@ -133,6 +133,7 @@ export type BookDetail = {
seriesIndex: number | null;
seriesMemberships?: BookSeriesMembership[];
rating: number | null;
communityRatings: BookCommunityRating[];
coverSource: "extracted" | "custom" | null;
hardcoverEditionId: string | null;
providerIds: ProviderIds;
@@ -171,6 +172,7 @@ export type BookMetadataRefreshPreviewFields = {
seriesName?: string | null;
seriesIndex?: number | null;
seriesMemberships?: MetadataSeriesMembership[] | null;
communityRatings?: BookCommunityRating[];
coverUrl?: string;
googleBooksId?: string | null;
goodreadsId?: string | null;
+10
View File
@@ -37,6 +37,13 @@ export interface MetadataSeriesMembership {
seriesIndex?: number | null;
}
export interface BookCommunityRating {
provider: MetadataProviderKey;
rating: number;
ratingCount: number | null;
updatedAt: string | null;
}
export interface MetadataCandidate {
provider: MetadataProviderKey;
providerId: string;
@@ -63,6 +70,8 @@ export interface MetadataCandidate {
audibleId?: string;
chapters?: AudiobookChapter[];
comicMetadata?: ComicMetadataFields;
communityRating?: number;
communityRatingCount?: number;
}
export interface MetadataProviderInfo {
@@ -116,4 +125,5 @@ export interface MetadataSource {
durationSeconds: number | null;
abridged: boolean | null;
hardcoverEditionId: string | null;
communityRatings: BookCommunityRating[];
}
+15 -1
View File
@@ -14,6 +14,7 @@ export const BOOK_METADATA_LOCK_FIELDS = [
"genres",
"tags",
"rating",
"communityRating",
"narrators",
"durationSeconds",
"abridged",
@@ -58,7 +59,20 @@ export const BOOK_METADATA_LOCK_GROUPS: BookMetadataLockGroup[] = [
{
key: "core",
label: "Core metadata",
fields: ["title", "subtitle", "authors", "description", "publisher", "publishedYear", "language", "pageCount", "isbn13", "isbn10", "rating"],
fields: [
"title",
"subtitle",
"authors",
"description",
"publisher",
"publishedYear",
"language",
"pageCount",
"isbn13",
"isbn10",
"rating",
"communityRating",
],
},
{
key: "series",
@@ -10,6 +10,7 @@ export type MetadataField =
| "publishedYear"
| "language"
| "pageCount"
| "communityRating"
| "seriesName"
| "seriesIndex"
| "genres"
@@ -27,6 +28,7 @@ export const ALL_METADATA_FIELDS: MetadataField[] = [
"publishedYear",
"language",
"pageCount",
"communityRating",
"seriesName",
"seriesIndex",
"genres",