initial commit

This commit is contained in:
naiy committed 2026-07-28 19:39:45 +08:00
commit 5626fb6558
25 files changed
+3948

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
import apiFetch from '../utils/request'
import type { Book } from '../types'
import { getCatalog } from './catalog'
import moment from 'moment'
export function mappingCreationStatus(status: string): string {
switch (status) {
case '0':
return '完结'
case '1':
return '连载'
case '4':
return '断更'
default:
return '未知'
}
}
export async function getBookInfoRaw(bookId: string): Promise<any> {
const response = await apiFetch(`https://api5-sinfonlinec.jxbhmy.com/reading/bookapi/multi-detail/v?book_id=${bookId}&aid=13`)
const j: any = await response.json()
if (
typeof j === 'object' &&
j !== null &&
'data' in j &&
Array.isArray(j.data) &&
j.data.length > 0
) return j.data[0]
return null
}
export async function getBookInfo(bookId: string): Promise<Book> {
const bookInfo = await getBookInfoRaw(bookId)
if (!bookInfo) {
throw new Error('Book not found')
}
return {
book_id: bookInfo.book_id,
title: bookInfo.book_name || bookInfo.original_book_name,
author: bookInfo.author,
cover_url: bookInfo.thumb_url,
summary: bookInfo.abstract,
// volume_list: bookInfo.volume_list,
update_time: moment(bookInfo.last_chapter_first_pass_time).format('YYYY-MM-DD HH:mm:ss'),
status: mappingCreationStatus(bookInfo.creation_status),
// chapter_count: bookInfo.chapter_count,
} as Book
}
export async function getBookInfoAndCatalog(book: Book | string): Promise<Book> {
if (typeof book !== 'string') {
book = book.book_id
}
const bookInfo = await getBookInfo(book)
if (!bookInfo) {
throw new Error('Book not found')
}
const catalog = await getCatalog(bookInfo.book_id)
console.log('Catalog:', catalog)
bookInfo.volume_list = catalog.volume_list
bookInfo.chapter_list = catalog.chapter_list
return bookInfo
}
+53
View File
@@ -0,0 +1,53 @@
import apiFetch from '../utils/request'
import type { Book, CatalogResult, ChapterItem, VolumeItem } from '../types'
import moment from 'moment'
export async function getCatalogRaw(bookId: string): Promise<any> {
const response = await apiFetch(`https://fanqienovel.com/api/reader/directory/detail?bookId=${bookId}`)
const j: any = response.json()
if (j.code !== 0 || j.data.chapterListWithVolume.length === 0) {
throw new Error('Empty catalog')
}
return [j.data.chapterListWithVolume, j.data.allItemIds]
}
export async function getCatalog(bookId: string): Promise<CatalogResult> {
const r = await getCatalogRaw(bookId) as Array<unknown>
const catalogRaw: any = r[0] // chapter list with volume
const allItemIds = r[1] as string[] // all item ids
const vmap: Record<string, VolumeItem> = {} // title -> VolumeItem
const chapters: Array<ChapterItem> = []
// const str = ''
catalogRaw[0].map((item: any): void => {
// console.debug(item)
const chapterItem = {
item_id: item.itemId,
title: item.title,
// YYYY-MM-DD HH:mm:ss
update_time: moment(item.updateTime).format('YYYY-MM-DD HH:mm:ss') as string,
char_count: item.charCount,
volume_title: item.volume_name,
} as ChapterItem
chapters.push(chapterItem)
if (!vmap[item.volume_name]) {
vmap[item.volume_name] = {
title: item.volume_name,
book_id: bookId,
chapter_list: [],
} as VolumeItem
}
(vmap[item.volume_name] as VolumeItem).chapter_list.push(chapterItem)
})
return {
book_id: bookId,
volume_list: Object.values(vmap),
chapter_list: chapters,
all_item_ids: allItemIds,
} as CatalogResult
}
export async function enrichBookCatalog(book: Book): Promise<void> {
const catalog = await getCatalog(book.book_id)
book.volume_list = catalog.volume_list
book.chapter_list = catalog.chapter_list
}
+82
View File
@@ -0,0 +1,82 @@
import config, { type KeyInfo } from '../config'
import { get, post } from '.'
import { encryptKeyinfoBody, decryptKeyinfoResponse, decryptChapter, b64decode, b64encode } from '../utils/crypto'
import { read, write } from '../localStorage'
async function refreshKeyinfo(): Promise<void> {
const b = await encryptKeyinfoBody(config.currentConfig)
const res = await post('/reader/crypt/registerkey/v1', false, {}, b)
console.log(res)
const j = res.json()
console.log('response: ', j)
const ek = j?.data?.key
if (!ek) {
throw new Error('Failed to get key info')
}
const key = await decryptKeyinfoResponse(ek)
const keyinfo: KeyInfo = {
key,
keyver: j?.data?.keyver as number,
}
console.log('Refreshed key info:', keyinfo)
config.currentConfig.key_info = keyinfo
write('keyinfo', {
key: b64encode(key),
keyver: j?.data?.keyver as number,
}) // cache key info
}
async function ensureKeyinfo(expectedKeyVersion?: number): Promise<void> {
const keyinfo = config.currentConfig.key_info
const cachedKeyInfo = read('keyinfo')
console.log('cached key info: ', cachedKeyInfo)
if (cachedKeyInfo) {
const cki = {
key: b64decode(cachedKeyInfo.key),
keyver: cachedKeyInfo.keyver,
} as KeyInfo
/*
console.log({
expectedKeyVersion,
cki,
assert: cki.keyver === expectedKeyVersion, // assert key version
})
*/
if (typeof expectedKeyVersion === 'undefined' || cki.keyver === expectedKeyVersion) {
config.currentConfig.key_info = cki
return
}
}
if (!keyinfo) {
// get from zero
return await refreshKeyinfo()
}
if (keyinfo?.keyver !== expectedKeyVersion) {
// refresh from server
return await refreshKeyinfo()
}
}
export async function getChapter(itemId: string, _retry?: number): Promise<any> {
if (typeof _retry === 'undefined') _retry = 0
if (_retry > 5) {
throw new Error(`Failed to get chapter: ${itemId}`)
}
if (!config.currentConfig.key_info) {
await ensureKeyinfo()
}
const res = await get(`/reader/full/v1`, false, { item_id: itemId })
const j = res.json()?.data
if (!j) {
console.warn('Failed to get chapter: ', itemId, ', response: ', j)
return await getChapter(itemId, _retry + 1)
// throw new Error('Failed to get chapter')
}
if (j?.content === 'Invalid' || j?.key_version !== config.currentConfig.key_info?.keyver) { // keyreg expired
console.warn('Key reg expired, regster again and retrying...')
await ensureKeyinfo(parseInt(j?.key_version))
return await getChapter(itemId, _retry + 1)
}
j.content = await decryptChapter(j?.content, config.currentConfig)
return j
}
+68
View File
@@ -0,0 +1,68 @@
import apiFetch from '../utils/request'
import { getCookie } from '../localStorage'
import config from '../config'
export const baseUrl = 'https://api5-sinfonlinec.jxbhmy.com/redcandle/search';
export const baseQuery = {
aid: '1967',
app_name: 'novelapp'
}
export async function get(path: string, cookie?: boolean, query?: any, headers?: any, optionsOverride?: any): Promise<any> {
let url = baseUrl + path;
const sessid = getCookie('sessionid')
let ck = sessid ? `sessionid=${sessid}` : ''
const h = {
Cookie: cookie ? ck : '',
...headers
}
const deviceQuery = {
device_id: config.currentConfig.device_id,
iid: config.currentConfig.install_id,
}
const finalQuery = {
...baseQuery,
...deviceQuery,
...query
}
url += '?' + new URLSearchParams(finalQuery).toString();
const options = {
method: 'GET',
headers: h,
...optionsOverride
}
console.log('---start--- GET ', url, options)
const res = await apiFetch(url, options)
console.log('---complete--- GET ', url, res)
return res
}
export async function post(path: string, cookie?: boolean, query?: any, body?: any, headers?: any, optionsOverride?: any): Promise<any> {
let url = baseUrl + path;
const sessid = getCookie('sessionid')
let ck = sessid ? `sessionid=${sessid}` : ''
const h = {
Cookie: cookie ? ck : '',
...headers
}
const deviceQuery = {
device_id: config.currentConfig.device_id,
iid: config.currentConfig.install_id,
}
const finalQuery = {
...baseQuery,
...deviceQuery,
...query
}
url += '?' + new URLSearchParams(finalQuery).toString();
const options = {
method: 'POST',
headers: h,
body: body,
...optionsOverride
}
console.log('---start--- POST ', url, options)
const res = await apiFetch(url, options)
console.log('---complete--- POST ', url, res)
return res
}
+40
View File
@@ -0,0 +1,40 @@
import { fetch } from '../config'
export interface UserInfo {
id: string
username: string
avatar: string
desc: string
age: number
}
export let userInfo: UserInfo | null = null
export async function getUserInfo() {
if (userInfo) return userInfo
const response = await fetch('https://fanqienovel.com/api/user/info/v2')
const j = await response.json()
userInfo = j?.data
return userInfo
}
export async function checkLogin() {
// sessionid is HttpOnly. So we use fetch directly to access user api.
const response = await fetch('https://fanqienovel.com/api/user/info/v2')
const j = await response.json()
userInfo = {
id: j?.data?.id,
username: j?.data?.name,
avatar: j?.data?.avatar,
desc: j?.data?.desc,
age: j?.data?.age,
} as UserInfo
return j?.data?.id > 1
}
export default async function init(): Promise<void> {
await checkLogin()
if (userInfo) {
console.log('Hello, ', userInfo.username)
}
}