Add novel.ovh (#756)

* fix

* test

* fix
This commit is contained in:
Rider21
2024-02-07 20:51:25 +03:00
committed by GitHub
parent 1907814d87
commit db921e704f
12 changed files with 582 additions and 18 deletions
+32 -2
View File
@@ -3,6 +3,7 @@ import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
class freedlit implements Plugin.PluginBase {
id = "freedlit.space";
@@ -75,8 +76,13 @@ class freedlit implements Plugin.PluginBase {
const url = loadedCheerio(element).attr("href");
if (!name || !url) return;
const releaseTime = loadedCheerio(element).find('span[class="date"]').text();
chapters.push({ name, url, releaseTime, chapterNumber: chapterIndex + 1 });
const releaseDate = loadedCheerio(element).find('span[class="date"]').text();
chapters.push({
name,
url,
releaseTime: this.parseDate(releaseDate),
chapterNumber: chapterIndex + 1
});
});
novel.chapters = chapters;
@@ -111,6 +117,30 @@ class freedlit implements Plugin.PluginBase {
return novels;
}
parseDate = (dateString: string | undefined = "") => {
const months: { [key: string]: number } = {
января: 1,
февраля: 2,
марта: 3,
апреля: 4,
мая: 5,
июня: 6,
июля: 7,
августа: 8,
сентября: 9,
октября: 10,
ноября: 11,
декабря: 12,
};
const [day, month, year] = dateString.split(" ");
if (day && months[month] && year) {
return dayjs(year + "-" + months[month] + "-" + day).format("LL");
}
return dateString || null;
};
fetchImage = fetchFile;
filters = {
+3 -1
View File
@@ -68,7 +68,9 @@ class Bookriver implements Plugin.PluginBase {
chapters.push({
name: chapter.name,
url: this.site + "/reader/" + book?.slug + "/" + chapter.chapterId,
releaseTime: dayjs(chapter?.firstPublishedAt || chapter.createdAt).format("LLL"),
releaseTime: dayjs(
chapter.firstPublishedAt || chapter.createdAt || undefined,
).format("LLL"),
chapterNumber: chapterIndex + 1,
});
}
+29 -1
View File
@@ -4,6 +4,7 @@ import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import { defaultCover } from "@libs/defaultCover";
import dayjs from "dayjs";
class ficbook implements Plugin.PluginBase {
id = "ficbook";
@@ -99,7 +100,10 @@ class ficbook implements Plugin.PluginBase {
const releaseTime = loadedCheerio(element).find("div > span").attr("title");
chapters.push({
name, url: this.site + url, releaseTime, chapterNumber: chapterIndex + 1,
name,
url: this.site + url,
releaseTime,
chapterNumber: chapterIndex + 1,
});
});
@@ -160,6 +164,30 @@ class ficbook implements Plugin.PluginBase {
return novels;
}
parseDate = (dateString: string | undefined = "") => {
const months: { [key: string]: number } = {
января: 1,
февраля: 2,
марта: 3,
апреля: 4,
мая: 5,
июня: 6,
июля: 7,
августа: 8,
сентября: 9,
октября: 10,
ноября: 11,
декабря: 12,
};
const [day, month, year, , time] = dateString.split(" ");
if (day && month && year && months[month] && time) {
return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL");
}
return dateString || null;
};
fetchImage = fetchFile;
filters = {
+33 -2
View File
@@ -3,6 +3,7 @@ import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import { load as parseHTML } from "cheerio";
import dayjs from "dayjs";
class Jaomix implements Plugin.PluginBase {
id = "jaomix.ru";
@@ -98,8 +99,13 @@ class Jaomix implements Plugin.PluginBase {
const url = loadedCheerio(element).find("a").attr("href");
if (!name || !url) return;
const releaseTime = loadedCheerio(element).find("time").text();
chapters.push({ name, url, releaseTime, chapterNumber: totalChapters - chapterIndex });
const releaseDate = loadedCheerio(element).find("time").text();
chapters.push({
name,
url,
releaseTime: this.parseDate(releaseDate),
chapterNumber: totalChapters - chapterIndex
});
});
novel.chapters = chapters.reverse();
@@ -146,6 +152,31 @@ class Jaomix implements Plugin.PluginBase {
return novels;
}
parseDate = (dateString: string | undefined = "") => {
const months: { [key: string]: number } = {
Янв: 1,
Фев: 2,
Мар: 3,
Апр: 4,
Май: 5,
Июн: 6,
Июл: 7,
Авг: 8,
Сен: 9,
Окт: 10,
Ноя: 11,
Дек: 12,
};
const [time, day, month, year] = dateString.split(" ");
if (time && day && months[month] && year) {
return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL");
}
return dateString || null;
};
fetchImage = fetchFile;
filters = {
+415
View File
@@ -0,0 +1,415 @@
import { Plugin } from "@typings/plugin";
import { FilterTypes, Filters } from "@libs/filterInputs";
import { fetchApi, fetchFile } from "@libs/fetch";
import { NovelStatus } from "@libs/novelStatus";
import dayjs from "dayjs";
class novelOvh implements Plugin.PluginBase {
id = "novelovh";
name = "НовелОВХ";
site = "https://novel.ovh";
version = "1.0.0";
icon = "src/ru/novelOvh/icon.png";
async popularNovels(
pageNo: number,
{ showLatestNovels, filters }: Plugin.PopularNovelsOptions,
): Promise<Plugin.NovelItem[]> {
let url = this.site + "/novel?page=" + (pageNo - 1);
url += "&sort=" +
(showLatestNovels ? "updatedAt" : filters?.sort?.value || "averageRating") + ",desc";
const result = await fetchApi(url + "&_data=routes/reader/book/index");
const body = (await result.json()) as { books: BooksEntity[] };
const novels: Plugin.NovelItem[] = [];
body.books.forEach((novel) =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
url: this.site + "/novel/" + novel.slug,
}),
);
return novels;
}
async parseNovelAndChapters(novelUrl: string): Promise<Plugin.SourceNovel> {
const result = await fetchApi(
novelUrl + "?_data=routes/reader/book/$slug/index",
);
const json = (await result.json()) as responseNovel;
const novel: Plugin.SourceNovel = {
url: novelUrl,
name: json.book.name.ru,
cover: json.book.poster,
genres: json.book.labels?.map?.((label) => label.name).join(","),
summary: json.book.description,
status:
json.book.status == "ONGOING"
? NovelStatus.Ongoing
: NovelStatus.Completed,
};
json.book.relations?.forEach((person) => {
switch (person.type) {
case "AUTHOR":
novel.author = person.publisher.name;
case "ARTIST":
novel.artist = person.publisher.name;
}
});
const chapters: Plugin.ChapterItem[] = [];
json.chapters.forEach((chapter, chapterIndex) =>
chapters.push({
name:
chapter.title ||
"Том " + (chapter.volume || 0) + " " +
(chapter.name || "Глава " +
(chapter.number || json.chapters.length - chapterIndex)),
url: novelUrl + "/" + chapter.id + "/0",
releaseTime: dayjs(chapter.createdAt).format("LLL"),
chapterNumber: json.chapters.length - chapterIndex,
}),
);
novel.chapters = chapters.reverse();
return novel;
}
async parseChapter(chapterUrl: string): Promise<string> {
const result = await fetchApi(
"https://api.novel.ovh/v2/chapters/" + chapterUrl.split("/")[5],
);
const book = (await result.json()) as responseChapter;
const image = Object.fromEntries(
book?.pages?.map(({ id, image }) => [id, image]) || [],
);
const chapterText = this.jsonToHtml(book.content.content || [], image);
return chapterText;
}
async searchNovels(
searchTerm: string,
page: number | undefined = 1,
): Promise<Plugin.NovelItem[]> {
const url = `${this.site}/novel?search=${searchTerm}&page=${page - 1}`;
const result = await fetchApi(url + "&_data=routes/reader/book/index");
const body = (await result.json()) as { books: BooksEntity[] };
const novels: Plugin.NovelItem[] = [];
body.books.forEach((novel) =>
novels.push({
name: novel.name.ru,
cover: novel.poster,
url: this.site + "/novel/" + novel.slug,
}),
);
return novels;
}
jsonToHtml = (
json: ContentEntity[],
image: { [key: string]: string },
html: string = "",
) => {
json.forEach((element) => {
switch (element.type) {
case "image":
if (element.attrs?.pages?.[0]) {
html += `<img src="${image[element.attrs.pages[0]]}"/>`;
}
break;
case "hardBreak":
html += "<br>";
break;
case "horizontalRule":
case "delimiter":
html += '<h2 style="text-align: center">***</h2>';
break;
case "paragraph":
html +=
"<p" +
(element?.attrs?.textAlign
? ` style="text-align: ${element.attrs.textAlign}"`
: "") +
">" +
(element.content ? this.jsonToHtml(element.content, image) : "<br>") +
"</p>";
break;
case "text":
html += element.text;
break;
default:
html += JSON.stringify(element, null, "\t"); //maybe I missed something.
break;
}
});
return html;
};
fetchImage = fetchFile;
filters = {
sort: {
label: "Сортировка",
value: "averageRating",
options: [
{ label: "Кол-во просмотров", value: "viewsCount" },
{ label: "Кол-во лайков", value: "likesCount" },
{ label: "Кол-во глав", value: "chaptersCount" },
{ label: "Кол-во закладок", value: "bookmarksCount" },
{ label: "Рейтингу", value: "averageRating" },
{ label: "Дате создания", value: "createdAt" },
{ label: "Дате обновления", value: "updatedAt" },
],
type: FilterTypes.Picker,
},
} satisfies Filters;
}
export default new novelOvh();
interface BooksEntity {
id: string;
slug: string;
type: string;
serviceName: string;
poster: string;
background?: string | null;
featuredCharacter?: null;
featuredCharacterBackground?: null;
featuredCharacterAnimation?: null;
featuredCharacterAnimationWithMask?: null;
featuredCharacterAnimationFirstFrame?: null;
status: string;
contentStatus: string;
name: Name;
altNames?: AltNamesEntity[] | null;
country: string;
year: number;
formats?: null[] | null;
featured: boolean;
viewsCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
chaptersCount: number;
createdAt: string;
updatedAt: string;
}
interface Name {
ru: string;
original: string;
en: string;
}
interface AltNamesEntity {
language: string;
name: string;
}
interface responseNovel {
book: Book;
branches: BranchesEntity[];
chapters: ChaptersEntity[];
related?: RelatedEntityOrBook[] | null;
associated?: AssociatedEntity[] | null;
chapterLikes?: null[] | null;
covers?: CoversEntity[] | null;
promotionVideos?: null[] | null;
folderItems?: null[] | null;
contentNotices?: null[] | null;
}
interface Book {
id: string;
slug: string;
type: string;
serviceName: string;
poster: string;
background?: null;
featuredCharacter?: null;
featuredCharacterBackground?: null;
featuredCharacterAnimation?: null;
featuredCharacterAnimationWithMask?: null;
featuredCharacterAnimationFirstFrame?: null;
contentBlockedCountries?: null[] | null;
status: string;
contentStatus: string;
moderationStatus: string;
name: Name;
altNames?: null[] | null;
externalLinks?: string[] | null;
description: string;
country: string;
year: number;
formats?: null[] | null;
featured: boolean;
viewsCount: number;
viewsDayCount: number;
viewsWeekCount: number;
viewsMonthCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
chaptersCount: number;
labels?: LabelsEntity[] | null;
relations?: RelationsEntity[] | null;
createdById: string;
createdAt: string;
updatedAt: string;
rating?: null;
}
interface LabelsEntity {
id: string;
slug: string;
name: string;
}
interface RelationsEntity {
type: string;
publisher: Publisher;
}
interface Publisher {
id: string;
kind: string;
slug: string;
name: string;
altNames?: (AltNamesEntity | null)[] | null;
poster: string;
background?: null;
trusted: boolean;
donationsEnabled: boolean;
vkGroupId?: null;
vkGroupName?: null;
tonAddress?: null;
createdAt: string;
updatedAt: string;
}
interface BranchesEntity {
id: string;
licensed: boolean;
editorsChoice: boolean;
chaptersCount: number;
createdAt: string;
updatedAt: string;
publishers?: PublishersEntity[] | null;
subscription?: null;
bookmark?: null;
}
interface PublishersEntity {
id: string;
kind: string;
slug: string;
name: string;
altNames?: AltNamesEntity[] | null;
poster: string;
background?: null;
trusted: boolean;
donationsEnabled: boolean;
vk?: null;
tonAddress?: null;
createdAt: string;
updatedAt: string;
}
interface ChaptersEntity {
id: string;
name?: string | null;
title?: string | null;
number: number;
volume: number;
likesCount: number;
commentsCount: number;
donut: boolean;
corrupted: boolean;
createdById: string;
branchId: string;
bookId: string;
createdAt: string;
expiredAt: string;
updatedAt: string;
}
interface RelatedEntityOrBook {
id: string;
type: string;
slug: string;
poster: string;
motion?: null;
status: string;
contentStatus: string;
name: Name;
country: string;
year: number;
formats?: null[] | null;
viewsCount: number;
likesCount: number;
bookmarksCount: number;
ratingVotesCount: number;
averageRating: number;
createdAt: string;
updatedAt: string;
}
interface AssociatedEntity {
id: string;
kind: string;
createdAt: string;
updatedAt: string;
book: RelatedEntityOrBook;
}
interface CoversEntity {
id: string;
volume: number;
image: string;
createdAt: string;
updatedAt: string;
}
interface responseChapter {
id: string;
name?: string;
title: string;
number: number;
volume: number;
likesCount: number;
commentsCount: number;
moderationStatus: string;
content: Content;
pages?: PagesEntity[];
donut: boolean;
corrupted: boolean;
createdById: string;
branchId: string;
bookId: string;
publishers?: PublishersEntity[] | null;
createdAt: string;
expiredAt: string;
updatedAt: string;
}
interface Content {
type: string;
content?: ContentEntity[] | null;
}
interface ContentEntity {
text?: string | null;
type: string;
attrs?: Attrs | null;
content?: Content[] | null;
}
interface Attrs {
textAlign?: string;
pages?: [string];
}
interface PagesEntity {
id: string;
image: string;
index: number;
commentsCount: number;
height: number;
width: number;
createdAt: string;
}
+1 -1
View File
@@ -69,7 +69,7 @@ class RNBH implements Plugin.PluginBase {
const json = (await result.json()) as { data: responseNovel };
const novel: Plugin.SourceNovel = {
url: json.data.url,
url: novelUrl,
name: json.data.names?.rus || json.data.names.eng,
cover: json.data.posters.medium,
summary: json.data.description,
+1 -1
View File
@@ -156,7 +156,7 @@ class ReN implements Plugin.PluginBase {
},
order: {
label: "Порядок",
value: "",
value: "-",
options: [
{ label: "По убыванию", value: "-" },
{ label: "По возрастанию", value: "" },
+4 -4
View File
@@ -66,13 +66,13 @@ class Smakolykytl implements Plugin.PluginBase {
"https://api.smakolykytl.site/api/user/projects/" + id + "/books",
);
const data = (await res.json()) as response;
data?.books?.forEach(
(volume) =>
volume?.chapters?.map((chapter) =>
data?.books?.forEach((volume) =>
volume?.chapters?.forEach((chapter) =>
chapters.push({
name: volume.title + " " + chapter.title,
releaseTime: dayjs(chapter.modifiedAt).format("LLL"),
url: this.site + "read/" + chapter.id,
releaseTime: dayjs(chapter.modifiedAt).format("LLL"),
chapterNumber: chapters.length + 1,
}),
),
);