forked from akai/readest
sync: also sync book metadata (#1611)
This commit is contained in:
@@ -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,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 })),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user