mirror of
https://github.com/naiyQAQ/fanqie-assistant.git
synced 2026-10-21 10:24:47 +00:00
initial commit
This commit is contained in:
commit
5626fb6558
25 files changed
+3948
No files matched your search
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user