diff --git a/icons/src/ru/novelovh/icon.png b/icons/src/ru/novelovh/icon.png new file mode 100644 index 0000000..7172c60 Binary files /dev/null and b/icons/src/ru/novelovh/icon.png differ diff --git a/plugins/russian/LitSpace.ts b/plugins/russian/LitSpace.ts index 3d21655..eeb6a83 100644 --- a/plugins/russian/LitSpace.ts +++ b/plugins/russian/LitSpace.ts @@ -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 = { diff --git a/plugins/russian/bookriver.ts b/plugins/russian/bookriver.ts index f9e980e..2170e77 100644 --- a/plugins/russian/bookriver.ts +++ b/plugins/russian/bookriver.ts @@ -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, }); } diff --git a/plugins/russian/ficbook.ts b/plugins/russian/ficbook.ts index 19f058c..881527c 100644 --- a/plugins/russian/ficbook.ts +++ b/plugins/russian/ficbook.ts @@ -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 = { diff --git a/plugins/russian/jaomix.ts b/plugins/russian/jaomix.ts index d0f79d8..a13a5fd 100644 --- a/plugins/russian/jaomix.ts +++ b/plugins/russian/jaomix.ts @@ -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 = { diff --git a/plugins/russian/novelOvh.ts b/plugins/russian/novelOvh.ts new file mode 100644 index 0000000..22463c2 --- /dev/null +++ b/plugins/russian/novelOvh.ts @@ -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 { + 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 { + 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 { + 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 { + 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 += ``; + } + break; + case "hardBreak": + html += "
"; + break; + case "horizontalRule": + case "delimiter": + html += '

***

'; + break; + case "paragraph": + html += + "" + + (element.content ? this.jsonToHtml(element.content, image) : "
") + + "

"; + 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; +} diff --git a/plugins/russian/ranobehub.ts b/plugins/russian/ranobehub.ts index 4c6a79c..a99a4ef 100644 --- a/plugins/russian/ranobehub.ts +++ b/plugins/russian/ranobehub.ts @@ -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, diff --git a/plugins/russian/renovels.ts b/plugins/russian/renovels.ts index 8363309..1cffaf6 100644 --- a/plugins/russian/renovels.ts +++ b/plugins/russian/renovels.ts @@ -156,7 +156,7 @@ class ReN implements Plugin.PluginBase { }, order: { label: "Порядок", - value: "", + value: "-", options: [ { label: "По убыванию", value: "-" }, { label: "По возрастанию", value: "" }, diff --git a/plugins/ukrainian/smakolykytl.ts b/plugins/ukrainian/smakolykytl.ts index 4442cc0..22b7d97 100644 --- a/plugins/ukrainian/smakolykytl.ts +++ b/plugins/ukrainian/smakolykytl.ts @@ -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, }), ), ); diff --git a/scripts/multisrc/ifreedom/template.ts b/scripts/multisrc/ifreedom/template.ts index 5a13658..a3bf5ae 100644 --- a/scripts/multisrc/ifreedom/template.ts +++ b/scripts/multisrc/ifreedom/template.ts @@ -3,7 +3,7 @@ import { Filters, FilterTypes } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; -// import { defaultCover } from "@libs/defaultCover"; +import dayjs from "dayjs"; export interface IfreedomMetadata { id: string; @@ -105,8 +105,13 @@ class IfreedomPlugin implements Plugin.PluginBase { const name = loadedCheerio(element).find("a").text(); const url = loadedCheerio(element).find("a").attr("href"); if (!loadedCheerio(element).find("label.buy-ranobe").length && name && url) { - const releaseTime = loadedCheerio(element).find("div.li-col2-ranobe").text().trim(); - chapters.push({ name, url, releaseTime, chapterNumber: totalChapters - chapterIndex }); + const releaseDate = loadedCheerio(element).find("div.li-col2-ranobe").text().trim(); + chapters.push({ + name, + url, + releaseTime: this.parseDate(releaseDate), + chapterNumber: totalChapters - chapterIndex + }); } }); @@ -156,5 +161,36 @@ class IfreedomPlugin 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, + }; + + if (dateString.includes(".")) { + const [day, month, year] = dateString.split("."); + if (day && month && year) { + return dayjs(year + "-" + month + "-" + day).format("LL"); + } + } else if (dateString.includes(" ")) { + const [day, month] = dateString.split(" "); + if (day && months[month]) { + const year = new Date().getFullYear(); + return dayjs(year + "-" + months[month] + "-" + day).format("LL"); + } + } + return dateString || null; + }; + fetchImage = fetchFile; } diff --git a/scripts/multisrc/readwn/template.ts b/scripts/multisrc/readwn/template.ts index fc0f3da..60d8b60 100644 --- a/scripts/multisrc/readwn/template.ts +++ b/scripts/multisrc/readwn/template.ts @@ -139,7 +139,7 @@ class ReadwnPlugin implements Plugin.PluginBase { return { name, url: this.site + url, - releaseTime: releaseTime, + releaseTime, chapterNumber: chapterIndex + 1, }; }) diff --git a/scripts/multisrc/rulate/template.ts b/scripts/multisrc/rulate/template.ts index 923e45c..15bd5e9 100644 --- a/scripts/multisrc/rulate/template.ts +++ b/scripts/multisrc/rulate/template.ts @@ -3,7 +3,7 @@ import { Filters, FilterTypes } from "@libs/filterInputs"; import { Plugin } from "@typings/plugin"; import { NovelStatus } from "@libs/novelStatus"; import { load as parseHTML } from "cheerio"; -// import { defaultCover } from "@libs/defaultCover"; +import dayjs from "dayjs"; export interface RulateMetadata { id: string; @@ -152,7 +152,7 @@ class RulatePlugin implements Plugin.PluginBase { chapters.push({ name: chapterName, url: this.site + chapterUrl, - releaseTime: releaseDate, + releaseTime: this.parseDate(releaseDate), chapterNumber: chapterIndex + 1, }); } @@ -210,6 +210,28 @@ class RulatePlugin 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 && months[month] && year && time) { + return dayjs(year + "-" + months[month] + "-" + day + " " + time).format("LLL"); + } + return dateString || null; + }; + fetchImage = fetchFile; }