Files
lnreader-plugins/plugins/russian/neobook.ts
T
Rajarshee Chatterjee 20c2b75b01 refactor: Overhaul Plugin Architecture & Playground UI (#1846)
* refactor: Move Plugins Out and Consolidate with Multisrc

* feat: Overhaul Playground UI

* feat: Add Filters to New UI

* feat: Persist Selected Plugin

* feat: Add Direct Parse Novel/Chapter Options and Feedback Toasts

[skip ci]

* feat: UX Improvements

* chore: Update Plugin Path in Publish Workflow

* fix: Add Backward Compatibility

* chore: Update Plugin Publishing Script
2025-10-20 22:54:08 +05:30

457 lines
11 KiB
TypeScript

import { Plugin } from '@/types/plugin';
import { FilterTypes, Filters } from '@libs/filterInputs';
import { defaultCover } from '@libs/defaultCover';
import { fetchApi } from '@libs/fetch';
import { NovelStatus } from '@libs/novelStatus';
const statusKey: Record<string, string> = {
'0': NovelStatus.Unknown,
'1': NovelStatus.Ongoing,
'2': NovelStatus.Completed,
'3': NovelStatus.OnHiatus,
'4': NovelStatus.Cancelled,
};
class Neobook implements Plugin.PluginBase {
id = 'neobook';
name = 'Neobook';
site = 'https://neobook.org';
apiSite = 'https://api.neobook.org/';
version = '1.0.2';
icon = 'src/ru/neobook/icon.png';
async fetchNovels(
page: number,
{
filters,
showLatestNovels,
}: Plugin.PopularNovelsOptions<typeof this.filters>,
searchTerm?: string,
): Promise<Plugin.NovelItem[]> {
const formData = new FormData();
formData.append('version', '4.4');
formData.append('uid', '0');
formData.append('utoken', '');
formData.append('resource', 'general');
formData.append('action', 'get_bundle');
formData.append('bundle', 'bundle_books');
formData.append('target', 'feed');
formData.append('page', page.toString());
formData.append('filter_category_id', filters?.category?.value || '0');
formData.append('filter_completed', '-1');
formData.append('filter_search', searchTerm || '');
formData.append('filter_tags', filters?.tags?.value || '');
formData.append(
'filter_sort',
showLatestNovels ? 'new' : filters?.sort?.value || 'popular',
);
formData.append('filter_timeread', filters?.timeread?.value || '0-999999');
const { bundle_books }: { bundle_books: BundleBooks } = await fetchApi(
this.apiSite,
{
method: 'post',
body: formData,
referrer: this.site,
},
).then(res => res.json());
const novels: Plugin.NovelItem[] = [];
if (bundle_books.feed?.length) {
bundle_books.feed?.forEach(novel =>
novels.push({
name: novel.title,
cover: novel?.attachment?.image?.m || defaultCover,
path: novel.token + '/',
}),
);
}
return novels;
}
popularNovels = this.fetchNovels;
async searchNovels(
searchTerm: string,
page: number,
): Promise<Plugin.NovelItem[]> {
const defaultOptions: any = {
filters: undefined,
showLatestNovels: false,
};
return this.fetchNovels(page, defaultOptions, searchTerm);
}
async parseNovel(novelPath: string): Promise<Plugin.SourceNovel> {
const body = await fetchApi(this.resolveUrl(novelPath, true)).then(res =>
res.text(),
);
const novel: Plugin.SourceNovel = {
path: novelPath,
name: '',
cover: defaultCover,
};
const bookRaw = body.match(/var postData = ({.*?});/);
if (bookRaw instanceof Array && bookRaw[1]) {
const book: Novels = JSON.parse(bookRaw[1]);
novel.name = book.title;
novel.summary = book.text?.replace?.(/<br>/g, '\n') || book.text_fix;
novel.author =
book.user && book.user.firstname && book.user.lastname
? book.user.firstname + ' ' + book.user.lastname
: book.user?.initials || '';
novel.status = statusKey[book.status || '0'] || NovelStatus.Unknown;
if (book.attachment?.image?.m) novel.cover = book.attachment.image.m;
if (book.tags?.length) novel.genres = book.tags.join(',');
const chapters: Plugin.ChapterItem[] = [];
book.chapters?.forEach((chapter, chapterIndex) => {
if (chapter.access == '1' && chapter.status == '1') {
chapters.push({
name: chapter.title || 'Глава ' + (chapterIndex + 1),
path: `?book=${book.token}&chapter=${chapter.token}`,
releaseTime: null,
chapterNumber: Number(chapter.sort) || chapterIndex + 1,
});
}
});
novel.chapters = chapters;
}
return novel;
}
async parseChapter(chapterPath: string): Promise<string> {
const body = await fetchApi(this.resolveUrl(chapterPath)).then(res =>
res.text(),
);
const bookRaw = body.match(/var data = ({.*?});/);
let chapterText = '';
if (bookRaw instanceof Array && bookRaw[1]) {
const token = chapterPath.split('=')[2];
const book: Novels = JSON.parse(bookRaw[1]);
const chapter = book.chapters?.find?.(chapter => chapter.token == token);
chapterText = (chapter?.data?.html || '').replace(/<br>/g, '');
}
return chapterText;
}
resolveUrl = (path: string, isNovel?: boolean) =>
this.site + (isNovel ? '/book/' : '/reader/') + path;
filters = {
sort: {
label: 'Сортировка:',
value: 'popular',
options: [
{ label: 'Сначала популярные', value: 'popular' },
{ label: 'Сначала новые', value: 'new' },
{ label: 'В случайном порядке', value: 'rand' },
],
type: FilterTypes.Picker,
},
timeread: {
label: 'Время прочтения:',
value: '0-999999',
options: [
{ label: 'Все', value: '0-999999' },
{ label: 'Менее 15 минут', value: '0-900' },
{ label: '15-30 минут', value: '900-1800' },
{ label: '30-60 минут', value: '1800-3600' },
{ label: '1-2 часа', value: '3600-7200' },
{ label: 'Более 2 часов', value: '7200-999999' },
],
type: FilterTypes.Picker,
},
category: {
label: 'Жанр:',
value: '',
options: [
{ label: 'Все', value: '' },
{ label: 'Антиутопия', value: '10' },
{ label: 'Детектив', value: '13' },
{ label: 'Детские книги', value: '14' },
{ label: 'Драма', value: '15' },
{ label: 'Другое', value: '16' },
{ label: 'История', value: '18' },
{ label: 'Мелодрама', value: '21' },
{ label: 'Мистика', value: '22' },
{ label: 'Научная фантастика', value: '23' },
{ label: 'Нон-фикшн', value: '35' },
{ label: 'Подростки и молодежь', value: '26' },
{ label: 'Постапокалипсис', value: '27' },
{ label: 'Поэзия', value: '28' },
{ label: 'Приключения', value: '29' },
{ label: 'Рассказ', value: '34' },
{ label: 'Роман', value: '36' },
{ label: 'Творчество', value: '40' },
{ label: 'Триллер', value: '91' },
{ label: 'Ужасы', value: '90' },
{ label: 'Фантастика', value: '41' },
{ label: 'Фанфик', value: '42' },
{ label: 'Фэнтези', value: '44' },
{ label: 'Эротика', value: '46' },
{ label: 'Юмор', value: '47' },
],
type: FilterTypes.Picker,
},
tags: {
label: 'Тэги:',
value: '',
type: FilterTypes.TextInput,
},
} satisfies Filters;
}
export default new Neobook();
type BundleBooks = {
categories: any[];
topSection: any[];
feed: Novels[];
};
type BottomSection = {
id: string;
boost: string;
preferenceid: string;
type_id: string;
contentTypeid: string;
name: string;
title: string;
more: string;
part: string;
items: Novels[];
};
type Novels = {
status?: string;
error?: string;
login?: Login;
requestid?: string;
bundleBooks?: BundleBooks;
available?: string;
boosted?: string;
id?: string;
token: string;
type?: string;
typeid?: string;
verified?: string;
audio?: string;
labelid?: string;
labelTitle?: string;
languageid?: string;
languageTitle?: string;
categoryid?: string;
categoryTitle?: string;
price?: Price;
priceSpecial?: Price;
priceDiscount?: string;
title: string;
text: string;
text_fix: string;
copyright?: string;
align?: string;
completed?: string;
timereadS?: string;
views?: string;
likes?: string;
comments?: string;
bookmarks?: string;
isLike?: string;
isBookmark?: string;
isPurchase?: string;
isSelf?: string;
dateEdit?: string;
datePublish?: string;
link_path?: string;
user?: NovelsUser;
attachment?: Attachment;
adult?: string;
adultPlaceholder?: string;
nft?: string;
feed?: Feed;
counters?: NovelsCounters;
rating?: Rating;
readTime?: ReadTime;
readProgress?: ReadProgress;
tags?: string[];
chapters?: Chapter[];
lastComments?: any[];
bottomSection?: BottomSection;
carouselItems?: Novels[];
book?: Novels;
activeChapterIndex?: number;
};
type Attachment = {
has: string;
id: string;
typeid: string;
userid: string;
width: string;
height: string;
duration: string;
image: Image;
video: string;
origin: string;
ext: string;
};
type Image = {
s: string;
m: string;
l: string;
has?: string;
};
type Chapter = {
id: string;
token: string;
status: string;
sort: string;
free: string;
title: string;
words: string;
access: string;
loaded: string;
data?: Data;
};
type Data = {
available: string;
access: string;
id: string;
token: string;
postid: string;
free: string;
title: string;
html: string;
dateEdit: string;
datePublish: string;
attachment: Attachment;
lastComments: any[];
};
type NovelsCounters = {
impressions: Bookmarks;
views: Bookmarks;
likes: Bookmarks;
comments: Bookmarks;
bookmarks: Bookmarks;
};
type Bookmarks = {
value: string;
formatted: string;
};
type Feed = {
name: string;
canRemoved: string;
};
type Login = {
uid: string;
utoken: string;
has: string;
banned: string;
verified: string;
acc: string;
arm: string;
grm: string;
counters: LoginCounters;
user: LoginUser;
pro: any[];
earn: any[];
boost: any[];
deposit: any[];
affiliate: any[];
preferences: any[];
};
type LoginCounters = {
notifications: string;
messages: string;
};
type LoginUser = {
id: string;
username: string;
firstname: string;
lastname: string;
photo: string;
status: string;
balance: Balance;
connectedService: string;
};
type Balance = {
neo: Price;
};
type Price = {
has: string;
value: string;
currency: Currency;
formatted: string;
deadline: string;
};
type Currency = 'NEO';
type Rating = {
value: string;
count: string;
my: string;
};
type ReadProgress = {
chapter: string;
progress: string;
};
type ReadTime = {
hours: string;
minutes: string;
};
type NovelsUser = {
available: string;
boosted: string;
id: string;
status: string;
verified: string;
metrica: string;
cover: Image;
photo: Image;
username: string;
firstname: string;
lastname: string;
initials: string;
link: string;
about: string;
subscriptions: string;
subscribers: string;
publications: string;
books: string;
poems: string;
posts: string;
drafts: string;
isPro: string;
isAdult: string;
isBanned: string;
isSubscription: string;
isBlock: string;
gr: string;
};