feat(metadata): add full publication date support (#598)

- Add a persisted publication date while continuing to derive publication years.

- Thread full dates through metadata import, editing, filters, sorting, write-back, and sync payloads.

- Cover provider parsing, DTO validation, jump buckets, and date formatting with tests.

Closes #596
This commit is contained in:
Chris Mansell
2026-07-09 08:17:53 -06:00
committed by GitHub
parent dbc2bdba73
commit 729b221427
138 changed files with 14383 additions and 129 deletions
@@ -19,6 +19,11 @@ describe("jumpBucketKindForSort", () => {
expect(jumpBucketKindForSort([{ field: "publishedYear", dir: "desc" }])).toBe("year");
});
it("returns year for publishedDate sorts", () => {
expect(jumpBucketKindForSort([{ field: "publishedDate", dir: "asc" }])).toBe("year");
expect(jumpBucketKindForSort([{ field: "publishedDate", dir: "desc" }])).toBe("year");
});
it("returns letter for an empty sort (defaults to title asc)", () => {
expect(jumpBucketKindForSort([])).toBe("letter");
});
+1
View File
@@ -10,6 +10,7 @@ export interface BookDockMetadata {
narrators?: string[];
description?: string;
publisher?: string;
publishedDate?: string;
publishedYear?: number;
language?: string;
pageCount?: number;
+3
View File
@@ -54,6 +54,7 @@ export type BookCard = {
seriesIndex: number | null;
seriesMemberships?: BookSeriesMembership[];
files: BookFileRef[];
publishedDate: string | null;
publishedYear: number | null;
language: string | null;
genres: string[];
@@ -126,6 +127,7 @@ export type BookDetail = {
isbn10: string | null;
isbn13: string | null;
publisher: string | null;
publishedDate: string | null;
publishedYear: number | null;
language: string | null;
pageCount: number | null;
@@ -169,6 +171,7 @@ export type BookMetadataRefreshPreviewFields = {
authors?: string[];
genres?: string[];
publisher?: string | null;
publishedDate?: string | null;
publishedYear?: number | null;
language?: string | null;
pageCount?: number | null;
+3
View File
@@ -3,6 +3,7 @@ export const CORE_BOOK_FILE_WRITE_FIELDS = [
"subtitle",
"description",
"publisher",
"publishedDate",
"publishedYear",
"language",
"pageCount",
@@ -60,6 +61,7 @@ export const BOOK_FILE_WRITE_FIELD_LABELS = {
subtitle: "Subtitle",
description: "Description",
publisher: "Publisher",
publishedDate: "Published date",
publishedYear: "Published year",
language: "Language",
pageCount: "Page count",
@@ -122,6 +124,7 @@ export const AUDIO_BOOK_FILE_WRITE_FIELDS = [
"subtitle",
"authors",
"narrators",
"publishedDate",
"publishedYear",
"publisher",
"description",
+1
View File
@@ -22,6 +22,7 @@ export type JumpBucketsResponse = {
const KIND_BY_PRIMARY_SORT_FIELD: Partial<Record<SortField, JumpBucketKind>> = {
title: "letter",
author: "letter",
publishedDate: "year",
publishedYear: "year",
};
+1
View File
@@ -228,6 +228,7 @@ export interface KoreaderCatalogBookDetail extends KoreaderCatalogBookListItem {
subtitle: string | null;
description: string | null;
publisher: string | null;
publishedDate: string | null;
publishedYear: number | null;
language: string | null;
isbn10: string | null;
+2
View File
@@ -66,6 +66,7 @@ export interface MetadataCandidate {
authors?: string[];
description?: string;
publisher?: string;
publishedDate?: string;
publishedYear?: number;
language?: string;
pageCount?: number;
@@ -126,6 +127,7 @@ export interface MetadataSource {
subtitle: string | null;
description: string | null;
publisher: string | null;
publishedDate: string | null;
publishedYear: number | null;
language: string | null;
pageCount: number | null;
+6
View File
@@ -17,6 +17,7 @@ import type { CommunityRatingProviderKey } from "./metadata-fetch";
* - `library` - resolved via `books.library_id` join to `libraries.name`
* - `format` - resolved via `book_files.format` (primary file)
* - `isbn` - matches both `isbn10` and `isbn13` in `book_metadata`
* - `publishedDate` - uses full dates when available and falls back to published year
* - `lockStatus` - derived from `book_metadata.locked_fields` (non-empty array = locked)
* - `seriesStatus` - computed per-user: "up next in series" (next unstarted book whose earlier series entries are all finished)
*/
@@ -26,6 +27,7 @@ export type RuleField =
| "language"
| "series"
| "seriesIndex"
| "publishedDate"
| "publishedYear"
| "pageCount"
| "author"
@@ -89,6 +91,7 @@ export const FIELD_OPERATORS: Record<RuleField, RuleOperator[]> = {
collection: ["includesAny", "excludesAll", "isEmpty", "isNotEmpty"],
library: ["includesAny", "excludesAll"],
format: ["includesAny", "excludesAll"],
publishedDate: ["before", "after", "between", "withinLast", "isEmpty", "isNotEmpty"],
publishedYear: ["eq", "notEq", "gt", "gte", "lt", "lte", "between", "isEmpty", "isNotEmpty"],
seriesIndex: ["eq", "notEq", "gt", "gte", "lt", "lte", "between", "isEmpty", "isNotEmpty"],
pageCount: ["gt", "gte", "lt", "lte", "between", "isEmpty", "isNotEmpty"],
@@ -181,6 +184,7 @@ export type GroupRule = {
* - `finishedAt` - from `user_book_status.finished_at` (per-user, correlated subquery)
* - `rating` - from `user_book_ratings.rating` (per-user, correlated subquery)
* - `format` - from `book_files.format` for the primary file (correlated subquery)
* - `publishedDate` - uses full dates when available and falls back to published year
* - `random` - day-seeded pseudorandom based on book id and user id
*
* Fields marked "per-user, correlated subquery" require an authenticated userId and
@@ -193,6 +197,7 @@ export type SortField =
| "seriesIndex"
| "addedAt"
| "updatedAt"
| "publishedDate"
| "publishedYear"
| "pageCount"
| "rating"
@@ -215,6 +220,7 @@ export const SORT_FIELDS: SortField[] = [
"seriesIndex",
"addedAt",
"updatedAt",
"publishedDate",
"publishedYear",
"pageCount",
"rating",