Files
bookdownload/src/db/pocketBase.ts

226 lines
7.9 KiB
TypeScript

import type { Db } from "./interfaces";
import PocketBase from "pocketbase";
import type { CollectionModel } from "pocketbase";
import { PocketBaseConfig } from "../config";
import { QdChapterInfo, QdBookInfo, PagedData } from "../types";
import { hash_qdchapter_info } from "../utils/qd";
const QD_CHAPTERS_FIELDS = [
{
'name': 'chapterId',
'type': 'number',
'required': true,
},
{
'name': 'bookId',
'type': 'number',
'required': true,
},
{
'name': 'time',
'type': 'number',
'required': true,
},
{
'name': 'hash',
'type': 'text',
'required': true,
},
{
'name': 'data',
'type': 'json',
'required': true,
}
];
const QD_BOOKS_FIELDS = [
{
'name': 'bookId',
'type': 'number',
'required': true,
},
{
'name': 'name',
'type': 'text',
'required': true,
},
{
'name': 'data',
'type': 'json',
'required': true,
}
];
const QD_CHAPTERS_INDEXES = [
'CREATE INDEX `idx_{name}_cid` ON `{name}` (chapterId)',
'CREATE INDEX `idx_{name}_bid` ON `{name}` (bookId)',
'CREATE INDEX `idx_{name}_hash` ON `{name}` (chapterId,bookId,hash)',
]
const QD_BOOKS_INDEXES = [
'CREATE UNIQUE INDEX `idx_{name}_bid` ON `{name}` (bookId)',
'CREATE INDEX `idx_{name}_name` ON `{name}` (name)',
]
export class PocketBaseDb implements Db {
client: PocketBase;
cfg: PocketBaseConfig;
use_token: boolean = false;
constructor(cfg: PocketBaseConfig) {
this.cfg = cfg;
this.client = new PocketBase(cfg.url);
}
async auth() {
await this.client.collection('_superusers').authWithPassword(this.cfg.username, this.cfg.password);
if (!this.client.authStore.isValid) {
throw new Error('Failed to authenticate with PocketBase. Please check your credentials.');
}
localStorage.setItem(`${this.cfg.url}/${this.cfg.username}.token`, this.client.authStore.token);
}
async _collections() {
try {
return await this.client.collections.getFullList({filter: this.cfg.prefix ? `name ~ "${this.cfg.prefix}%"` : undefined});
} catch (e) {
if (this.use_token) {
this.use_token = false;
this.client.authStore.clear();
await this.auth();
return await this.client.collections.getFullList({filter: this.cfg.prefix ? `name ~ "${this.cfg.prefix}%"` : undefined});
}
throw e;
}
}
async init() {
const token = localStorage.getItem(`${this.cfg.url}/${this.cfg.username}.token`);
if (token) {
this.client.authStore.save(token);
this.use_token = true;
} else {
await this.auth();
}
const collections = await this._collections();
const collectionNames = new Set(collections.map(c => c.name));
if (!collectionNames.has(`${this.cfg.prefix}qd_chapters`)) {
await this.createCollection('qd_chapters', QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES);
} else {
const target = collections.find(c => c.name === `${this.cfg.prefix}qd_chapters`)!;
if (!this.checkCollection(target, QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES)) {
await this.updateCollection('qd_chapters', QD_CHAPTERS_FIELDS, QD_CHAPTERS_INDEXES);
}
}
if (!collectionNames.has(`${this.cfg.prefix}qd_books`)) {
await this.createCollection('qd_books', QD_BOOKS_FIELDS, QD_BOOKS_INDEXES);
} else {
const target = collections.find(c => c.name === `${this.cfg.prefix}qd_books`)!;
if (!this.checkCollection(target, QD_BOOKS_FIELDS, QD_BOOKS_INDEXES)) {
await this.updateCollection('qd_books', QD_BOOKS_FIELDS, QD_BOOKS_INDEXES);
}
}
}
async createCollection(name: string, fields: Record<string, unknown>[], indexes: string[]) {
const nindexes = indexes.map(i => i.replaceAll('{name}', `${this.cfg.prefix}${name}`));
await this.client.collections.create({
name: `${this.cfg.prefix}${name}`,
type: 'base',
fields: fields,
indexes: nindexes,
});
}
async updateCollection(name: string, fields: Record<string, unknown>[], indexes: string[]) {
const nidexes = indexes.map(i => i.replaceAll('{name}', `${this.cfg.prefix}${name}`));
await this.client.collections.update(`${this.cfg.prefix}${name}`, {
fields: fields,
indexes: nidexes,
});
}
checkCollection(col: CollectionModel, fields: Record<string, unknown>[], indexes: string[]) {
for (const field of fields) {
const name = field.name;
const target = col.fields.find(f => f.name === name);
if (!target) {
return false;
}
for (const key in field) {
if (field[key] !== target[key]) {
return false;
}
}
}
for (const index of indexes) {
const tindex = index.replaceAll('{name}', col.name);
if (!col.indexes.includes(tindex)) {
return false;
}
}
return true;
}
async saveQdChapter(info: QdChapterInfo) {
const hash = hash_qdchapter_info(info);
const existed = await this.hasQdChapter(info.id, info.bookId, hash);
if (existed) {
console.log(`Chapter ${info.id} of book ${info.bookId} already exists in database, skipping`);
return;
}
info.hash = undefined;
const re = await this.client.collection(`${this.cfg.prefix}qd_chapters`).create({
chapterId: info.id,
bookId: info.bookId,
time: info.time,
hash: hash,
data: info,
});
console.log(re);
}
async saveQdBook(info: QdBookInfo) {
const id = await this.getQdBookId(info.id);
if (id) {
await this.client.collection(`${this.cfg.prefix}qd_books`).update(id, {
name: info.bookName,
data: info,
});
} else {
await this.client.collection(`${this.cfg.prefix}qd_books`).create({
bookId: info.id,
name: info.bookName,
data: info,
});
}
}
async getQdBookId(id: number): Promise<string | null> {
const records = await this.client.collection(`${this.cfg.prefix}qd_books`).getList(1, 1, {
filter: `bookId = ${id}`,
fields: 'id',
});
return records.totalItems > 0 ? records.items[0].id : null;
}
async hasQdChapter(id: number, bookId: number, hash: string) {
const records = await this.client.collection(`${this.cfg.prefix}qd_chapters`).getList(1, 1, {
filter: `chapterId = ${id} && bookId = ${bookId} && hash = "${hash}"`,
fields: 'id',
});
return records.totalItems > 0;
}
async getQdBooks(page: number, pageSize: number): Promise<PagedData<QdBookInfo>> {
const records = await this.client.collection(`${this.cfg.prefix}qd_books`).getList(page, pageSize, {
fields: 'data',
sort: 'bookId',
});
return {
total: records.totalItems,
page,
totalPages: records.totalPages,
pageSize,
items: records.items.map(item => item.data),
};
}
async getQdBook(id: number): Promise<QdBookInfo | undefined> {
const records = await this.client.collection(`${this.cfg.prefix}qd_books`).getList(1, 1, {
filter: `bookId = ${id}`,
fields: 'data',
});
return records.totalItems > 0 ? records.items[0].data : undefined;
}
close(): void {
this.client.cancelAllRequests();
this.client.authStore.clear();
}
}