sync: also sync book metadata (#1611)

This commit is contained in:
Huang Xin
2025-07-18 23:59:05 +08:00
committed by GitHub
parent 219edb9faf
commit d9199d1182
8 changed files with 45 additions and 14 deletions
@@ -60,7 +60,7 @@ export const useMetadataEdit = (metadata: BookMetadata | null) => {
const newMeta = { ...editedMeta } as { [key: string]: unknown };
switch (field) {
case 'subject':
newMeta['subject'] = value ? value.split(/,||、/).map((s) => s.trim()) : [];
newMeta['subject'] = value ? value.split(/,|;||、/).map((s) => s.trim()) : [];
break;
default:
newMeta[field] = value;
+18 -4
View File
@@ -18,7 +18,7 @@ import {
getPrimaryLanguage,
} from '@/utils/book';
import { partialMD5 } from '@/utils/md5';
import { BookDoc, DocumentLoader } from '@/libs/document';
import { BookDoc, DocumentLoader, EXTS } from '@/libs/document';
import {
DEFAULT_BOOK_LAYOUT,
DEFAULT_BOOK_STYLE,
@@ -205,6 +205,7 @@ export abstract class BaseAppService implements AppService {
hash,
format,
title: formatTitle(loadedBook.metadata.title),
sourceTitle: formatTitle(loadedBook.metadata.title),
author: formatAuthors(loadedBook.metadata.author, loadedBook.metadata.language),
primaryLanguage: getPrimaryLanguage(loadedBook.metadata.language),
createdAt: existingBook ? existingBook.createdAt : Date.now(),
@@ -215,8 +216,9 @@ export abstract class BaseAppService implements AppService {
};
// update book metadata when reimporting the same book
if (existingBook) {
existingBook.title = book.title;
existingBook.author = book.author;
existingBook.title = existingBook.title ?? book.title;
existingBook.sourceTitle = existingBook.sourceTitle ?? book.sourceTitle;
existingBook.author = existingBook.author ?? book.author;
existingBook.primaryLanguage = existingBook.primaryLanguage ?? book.primaryLanguage;
}
@@ -467,7 +469,19 @@ export abstract class BaseAppService implements AppService {
} else if (book.url) {
file = await this.fs.openFile(book.url, 'None');
} else {
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
// 0.9.64 has a bug that book.title might be modified but the filename is not updated
const bookDir = getDir(book);
const files = await this.fs.readDir(getDir(book), 'Books');
if (files.length > 0) {
const bookFile = files.find((f) => f.path.endsWith(`.${EXTS[book.format]}`));
if (bookFile) {
file = await this.fs.openFile(`${bookDir}/${bookFile.path}`, 'Books');
} else {
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
}
} else {
throw new Error(BOOK_FILE_NOT_FOUND_ERROR);
}
}
return { book, file, config: await this.loadBookConfig(book, settings) };
}
@@ -151,8 +151,10 @@ const indexedDBFileSystem: FileSystem = {
async removeDir() {
// Directories are virtual in IndexedDB; no-op
},
async readDir(path: string) {
async readDir(path: string, base: BaseDir) {
const { fp } = resolvePath(path, base);
const db = await openIndexedDB();
return new Promise<{ path: string; isDir: boolean }[]>((resolve, reject) => {
const transaction = db.transaction('files', 'readonly');
const store = transaction.objectStore('files');
@@ -162,8 +164,8 @@ const indexedDBFileSystem: FileSystem = {
const files = request.result as { path: string }[];
resolve(
files
.filter((file) => file.path.startsWith(path))
.map((file) => ({ path: file.path, isDir: false })),
.filter((file) => file.path.startsWith(fp))
.map((file) => ({ path: file.path.slice(fp.length + 1), isDir: false })),
);
};
+7 -3
View File
@@ -16,7 +16,7 @@ import { updateToc } from '@/utils/toc';
import { useSettingsStore } from './settingsStore';
import { useBookDataStore } from './bookDataStore';
import { useLibraryStore } from './libraryStore';
import { getPrimaryLanguage } from '@/utils/book';
import { formatTitle, getBaseFilename, getPrimaryLanguage } from '@/utils/book';
interface ViewState {
/* Unique key for each book view */
@@ -138,8 +138,12 @@ export const useReaderStore = create<ReaderStore>((set, get) => ({
const { book: bookDoc } = await new DocumentLoader(file).open();
updateToc(bookDoc, config.viewSettings?.sortedTOC ?? false);
// Set the book's language for formerly imported books, newly imported books have this field set
book.primaryLanguage =
book.primaryLanguage ?? getPrimaryLanguage(bookDoc.metadata.language);
if (!bookDoc.metadata.title) {
bookDoc.metadata.title = getBaseFilename(file.name);
}
book.sourceTitle = formatTitle(bookDoc.metadata.title);
const primaryLanguage = getPrimaryLanguage(bookDoc.metadata.language);
book.primaryLanguage = book.primaryLanguage ?? primaryLanguage;
book.metadata = book.metadata ?? bookDoc.metadata;
useBookDataStore.setState((state) => ({
booksData: {
+2 -1
View File
@@ -12,7 +12,8 @@ export interface Book {
filePath?: string;
hash: string;
format: BookFormat;
title: string;
title: string; // editable title from metadata
sourceTitle?: string; // parsed when the book is imported and used to locate the file
author: string;
group?: string; // deprecated in favor of groupId and groupName
groupId?: string;
+2
View File
@@ -3,12 +3,14 @@ export interface DBBook {
book_hash: string;
format: string;
title: string;
source_title?: string;
author: string;
group_id?: string;
group_name?: string;
tags?: string[];
progress?: [number, number];
metadata?: string | null;
created_at?: string;
updated_at?: string;
deleted_at?: string | null;
+2 -2
View File
@@ -14,7 +14,7 @@ export const getLibraryFilename = () => {
export const getRemoteBookFilename = (book: Book) => {
// S3 storage: https://docs.aws.amazon.com/zh_cn/AmazonS3/latest/userguide/object-keys.html
if (getStorageType() === 'r2') {
return `${book.hash}/${makeSafeFilename(book.title)}.${EXTS[book.format]}`;
return `${book.hash}/${makeSafeFilename(book.sourceTitle || book.title)}.${EXTS[book.format]}`;
} else if (getStorageType() === 's3') {
return `${book.hash}/${book.hash}.${EXTS[book.format]}`;
} else {
@@ -22,7 +22,7 @@ export const getRemoteBookFilename = (book: Book) => {
}
};
export const getLocalBookFilename = (book: Book) => {
return `${book.hash}/${makeSafeFilename(book.title)}.${EXTS[book.format]}`;
return `${book.hash}/${makeSafeFilename(book.sourceTitle || book.title)}.${EXTS[book.format]}`;
};
export const getCoverFilename = (book: Book) => {
return `${book.hash}/cover.png`;
+8
View File
@@ -41,11 +41,13 @@ export const transformBookToDB = (book: unknown, userId: string): DBBook => {
hash,
format,
title,
sourceTitle,
author,
groupId,
groupName,
tags,
progress,
metadata,
createdAt,
updatedAt,
deletedAt,
@@ -62,6 +64,8 @@ export const transformBookToDB = (book: unknown, userId: string): DBBook => {
group_name: groupName,
tags: tags,
progress: progress,
source_title: sourceTitle,
metadata: metadata ? JSON.stringify(metadata) : null,
created_at: new Date(createdAt ?? Date.now()).toISOString(),
updated_at: new Date(updatedAt ?? Date.now()).toISOString(),
deleted_at: deletedAt ? new Date(deletedAt).toISOString() : null,
@@ -79,6 +83,8 @@ export const transformBookFromDB = (dbBook: DBBook): Book => {
group_name,
tags,
progress,
source_title,
metadata,
created_at,
updated_at,
deleted_at,
@@ -94,6 +100,8 @@ export const transformBookFromDB = (dbBook: DBBook): Book => {
groupName: group_name,
tags: tags,
progress: progress,
sourceTitle: source_title,
metadata: metadata ? JSON.parse(metadata) : null,
createdAt: new Date(created_at!).getTime(),
updatedAt: new Date(updated_at!).getTime(),
deletedAt: deleted_at ? new Date(deleted_at).getTime() : null,