mirror of
https://github.com/lifegpc/bookdownload.git
synced 2026-06-06 05:38:46 +08:00
Add support to display book info and save book info to databases
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { IndexedDbConfig } from "../config";
|
||||
import type { QdChapterInfo } from "../types";
|
||||
import type { QdChapterInfo, QdBookInfo } from "../types";
|
||||
import { compress, isServiceWorker } from "../utils";
|
||||
import { hash_qdchapter_info } from "../utils/qd";
|
||||
import type { Db } from "./interfaces";
|
||||
@@ -148,6 +148,9 @@ export class IndexedDb implements Db {
|
||||
await save_data(this.qddb, 'chapters', info);
|
||||
}
|
||||
}
|
||||
async saveQdBook(info: QdBookInfo) {
|
||||
await save_data(this.qddb, 'books', info);
|
||||
}
|
||||
close() {
|
||||
this.qddb.close();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DbConfig, DbType } from "../config";
|
||||
import { IndexedDb } from "./indexedDb";
|
||||
import { PocketBaseDb } from "./pocketBase";
|
||||
import type { QdChapterInfo } from "../types";
|
||||
import type { QdChapterInfo, QdBookInfo } from "../types";
|
||||
|
||||
export interface Db {
|
||||
init(): Promise<void>;
|
||||
@@ -10,6 +10,11 @@ export interface Db {
|
||||
* @param info Chapter info to save. if id, bookId and hash are matched in the database, skip saving.
|
||||
*/
|
||||
saveQdChapter(info: QdChapterInfo): Promise<void>;
|
||||
/**
|
||||
* Save book info to database.
|
||||
* @param info Book info to save. if id is matched in the database, update the existing record.
|
||||
*/
|
||||
saveQdBook(info: QdBookInfo): Promise<void>;
|
||||
close(): void;
|
||||
}
|
||||
|
||||
@@ -18,11 +23,9 @@ export async function createDb(): Promise<Db> {
|
||||
await config.init();
|
||||
switch (config.DbType) {
|
||||
case DbType.IndexedDb:
|
||||
const db1 = new IndexedDb(config.IndexedDb);
|
||||
return db1;
|
||||
return new IndexedDb(config.IndexedDb);
|
||||
case DbType.PocketBase:
|
||||
const db2 = new PocketBaseDb(config.PocketBase);
|
||||
return db2;
|
||||
return new PocketBaseDb(config.PocketBase);
|
||||
default:
|
||||
throw new Error('Unsupported database type');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Db } from "./interfaces";
|
||||
import PocketBase from "pocketbase";
|
||||
import type { CollectionModel } from "pocketbase";
|
||||
import { PocketBaseConfig } from "../config";
|
||||
import { QdChapterInfo } from "../types";
|
||||
import { QdChapterInfo, QdBookInfo } from "../types";
|
||||
import { hash_qdchapter_info } from "../utils/qd";
|
||||
|
||||
const QD_CHAPTERS_FIELDS = [
|
||||
@@ -32,10 +32,31 @@ const QD_CHAPTERS_FIELDS = [
|
||||
'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_cid` ON `{name}` (chapterId)',
|
||||
'CREATE INDEX `idx_bid` ON `{name}` (bookId)',
|
||||
'CREATE INDEX `idx_hash` ON `{name}` (chapterId,bookId,hash)',
|
||||
'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 {
|
||||
@@ -60,6 +81,14 @@ export class PocketBaseDb implements Db {
|
||||
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[]) {
|
||||
await this.client.collections.create({
|
||||
@@ -114,6 +143,28 @@ export class PocketBaseDb implements Db {
|
||||
});
|
||||
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}"`,
|
||||
|
||||
67
src/models/QdBookInfo.tsx
Normal file
67
src/models/QdBookInfo.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Space, Card, Col, Row, Image, Descriptions, Collapse, Typography, Button } from 'antd';
|
||||
import type { QdBookInfo } from '../types';
|
||||
import { createDb } from '../db/interfaces';
|
||||
|
||||
interface QdBookInfoProps {
|
||||
info: QdBookInfo;
|
||||
}
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export default function QdBookInfo({ info }: QdBookInfoProps) {
|
||||
async function saveToDb() {
|
||||
const db = await createDb();
|
||||
await db.init();
|
||||
await db.saveQdBook(info);
|
||||
db.close();
|
||||
}
|
||||
return (
|
||||
<Space orientation="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Card title="书籍信息" size="small">
|
||||
<Row>
|
||||
<Col span={6}>
|
||||
<Image src={info.bookInfo.imgUrl} />
|
||||
</Col>
|
||||
<Col span={18}>
|
||||
<Descriptions column={1} size="small">
|
||||
<Descriptions.Item label="书名">
|
||||
{info.bookName}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="书籍ID">
|
||||
{info.id}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="标签">
|
||||
{info.tags.map(tag => tag.name).join(', ')}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="简介">
|
||||
{info.intro.split('\n').map((line, index) => (
|
||||
<>{line}<br /></>
|
||||
))}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
<Card title="操作" size="small">
|
||||
<Button type="primary" onClick={saveToDb}>保存到数据库</Button>
|
||||
</Card>
|
||||
<Card title="卷信息" size="small">
|
||||
<Collapse items={
|
||||
info.volumes.map(volume => ({
|
||||
key: volume.id,
|
||||
label: volume.name,
|
||||
extra: volume.isVip ? <span style={{ color: 'red' }}>VIP卷</span> : null,
|
||||
children: (<Space size="small" orientation="vertical">
|
||||
{volume.chapters.map(chapter => (
|
||||
<Text key={chapter.id} onClick={() => {
|
||||
const url = `https://www.qidian.com/chapter/${info.id}/${chapter.id}`;
|
||||
chrome.tabs.create({ url });
|
||||
}} style={{ cursor: 'pointer' }}>{chapter.name}</Text>
|
||||
))}
|
||||
</Space>)
|
||||
}))
|
||||
} />
|
||||
</Card>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import React from 'react';
|
||||
import { Descriptions, Card, Typography, Tag, Space, Button } from 'antd';
|
||||
import type { QdChapterInfo } from '../types';
|
||||
import { get_chapter_content, saveAsFile } from '../utils';
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { Message, QdChapterInfo } from "./types";
|
||||
import type { Message, QdChapterInfo, QdBookInfo } from "./types";
|
||||
import { getCurrentTab, parseUrlParams, sendMessageToTab } from "./utils";
|
||||
import * as styles from "./popup.module.css";
|
||||
import { Spin, Result } from "antd";
|
||||
import QdChapterInfoModel from "./models/QdChatperInfo";
|
||||
import QdBookInfoModel from "./models/QdBookInfo";
|
||||
|
||||
function PopupBody() {
|
||||
const [result, setResult] = useState<Message | null>(null);
|
||||
@@ -70,7 +71,10 @@ function PopupBody() {
|
||||
return <QdChapterInfoModel info={body} />;
|
||||
}
|
||||
if (result.ok && result.body?.type === 'QdBookInfo') {
|
||||
return <Result status="success" title={result.body.bookName} subTitle={`Book ID: ${result.body.id}`} />;
|
||||
const body: QdBookInfo = result.body;
|
||||
/**@ts-ignore*/
|
||||
delete body.type;
|
||||
return <QdBookInfoModel info={body} />;
|
||||
}
|
||||
return <Result status="error" title="错误" subTitle={result.msg || '未知错误'} />;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { BookGData, QdBookTag } from "./qdtypes";
|
||||
import type { BookGData, QdBookTag, Volume } from "./qdtypes";
|
||||
import type { SendMessage, Message } from "./types";
|
||||
import { QdBookTagType } from "./qdtypes";
|
||||
|
||||
let g_data: BookGData | undefined;
|
||||
|
||||
export const QD_CHAPTER_URLPATH_REGEX = /^\/chapter\/\d+\/(\d+)\/?$/;
|
||||
|
||||
function get_book_name() {
|
||||
const bookName = document.getElementById('bookName') as HTMLHeadingElement | null;
|
||||
if (!bookName) {
|
||||
@@ -53,6 +55,65 @@ function get_book_tags() {
|
||||
return tags;
|
||||
}
|
||||
|
||||
function get_book_intro() {
|
||||
const intro = document.getElementById('book-intro-detail') as HTMLParagraphElement | null;
|
||||
if (!intro) {
|
||||
throw new Error('Failed to find book intro element');
|
||||
}
|
||||
return intro.innerText.trim();
|
||||
}
|
||||
|
||||
function get_book_volumes() {
|
||||
const volumes: Volume[] = [];
|
||||
const vols = document.querySelectorAll('div.catalog-volume');
|
||||
for (const vol of vols) {
|
||||
const volInput = vol.querySelector('input.input-vol') as HTMLInputElement | null;
|
||||
if (!volInput) {
|
||||
throw new Error('Failed to find volume input element');
|
||||
}
|
||||
const volId = volInput.id;
|
||||
const volName = vol.querySelector('.volume-name') as HTMLElement | null;
|
||||
if (!volName) {
|
||||
throw new Error('Failed to find volume name element');
|
||||
}
|
||||
const firstNode = volName.firstChild;
|
||||
if (!firstNode) {
|
||||
throw new Error('Volume name element has no child');
|
||||
}
|
||||
if (firstNode.nodeType !== Node.TEXT_NODE) {
|
||||
throw new Error('Volume name element first child is not a text node');
|
||||
}
|
||||
const name = firstNode.textContent?.trim() || '';
|
||||
const vipNode = volName.querySelector('span.vip');
|
||||
const volume: Volume = {
|
||||
name,
|
||||
id: volId,
|
||||
isVip: !!vipNode,
|
||||
chapters: [],
|
||||
}
|
||||
const chs = vol.querySelectorAll('li.chapter-item');
|
||||
for (const ch of chs) {
|
||||
const chName = ch.querySelector('a.chapter-name') as HTMLAnchorElement | null;
|
||||
if (!chName) {
|
||||
throw new Error('Failed to find chapter name element');
|
||||
}
|
||||
const name = chName.innerText.trim();
|
||||
const href = new URL(chName.href);
|
||||
const match = href.pathname.match(QD_CHAPTER_URLPATH_REGEX);
|
||||
if (!match) {
|
||||
throw new Error(`Chapter URL does not match expected pattern: ${chName.href}`);
|
||||
}
|
||||
const chapterId = match[1];
|
||||
volume.chapters.push({
|
||||
name,
|
||||
id: parseInt(chapterId),
|
||||
});
|
||||
}
|
||||
volumes.push(volume);
|
||||
}
|
||||
return volumes;
|
||||
}
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const data = event.data;
|
||||
if (data && data['@type'] === 'g_data') {
|
||||
@@ -85,6 +146,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
bookName,
|
||||
id: g_data.pageJson.bookId,
|
||||
tags: get_book_tags(),
|
||||
intro: get_book_intro(),
|
||||
volumes: get_book_volumes(),
|
||||
},
|
||||
for: m.type,
|
||||
};
|
||||
|
||||
@@ -192,11 +192,18 @@ export type BookGData = {
|
||||
}
|
||||
}
|
||||
|
||||
export type Volume = {
|
||||
export type Chapter = {
|
||||
name: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export type Volume = {
|
||||
name: string;
|
||||
id: string;
|
||||
isVip: boolean;
|
||||
chapters: Chapter[];
|
||||
}
|
||||
|
||||
export enum QdBookTagType {
|
||||
System,
|
||||
Category,
|
||||
|
||||
@@ -22,12 +22,14 @@ export type QdChapterInfo = {
|
||||
hash?: string;
|
||||
}
|
||||
|
||||
export type QDBookInfo = {
|
||||
export type QdBookInfo = {
|
||||
bookInfo: QdTypes.BookGData;
|
||||
bookName: string;
|
||||
/**Book ID */
|
||||
id: number;
|
||||
tags: QdTypes.QdBookTag[];
|
||||
intro: string;
|
||||
volumes: QdTypes.Volume[];
|
||||
}
|
||||
|
||||
export type SendMessageMap = {
|
||||
@@ -42,7 +44,7 @@ export type SendMessage = DiscriminatedUnion<"type", SendMessageMap>;
|
||||
|
||||
export type MessageMap = {
|
||||
QdChapterInfo: QdChapterInfo,
|
||||
QdBookInfo: QDBookInfo,
|
||||
QdBookInfo: QdBookInfo,
|
||||
};
|
||||
|
||||
export type MessageBody = DiscriminatedUnion<"type", MessageMap>;
|
||||
|
||||
Reference in New Issue
Block a user