From 3ffa407b8681a2c6bf56498e8e10c4e1d8050a2b Mon Sep 17 00:00:00 2001 From: asteglor Date: Mon, 10 Feb 2025 17:07:02 +0200 Subject: [PATCH] add uaranobe.club (#1504) * add uaranobe.club * add uaranobeclub * uaranobe.club fix curl * review: change absolute paths into relative * fix: pagination * fix: plugin version ++ * fix: parseNovel * change plugin id * return full path * version++ * fix: return absolute path * return name * fix path * refactoring --- public/static/src/uk/uaranobeclub/icon.png | Bin 0 -> 3958 bytes src/plugins/ukrainian/uaranobeclub.ts | 275 +++++++++++++++++++++ 2 files changed, 275 insertions(+) create mode 100644 public/static/src/uk/uaranobeclub/icon.png create mode 100644 src/plugins/ukrainian/uaranobeclub.ts diff --git a/public/static/src/uk/uaranobeclub/icon.png b/public/static/src/uk/uaranobeclub/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..f7bedada81bb41d02bcff24cd67a1535f12144ec GIT binary patch literal 3958 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 Q3IG5A07*qoM6N<$g7QR0vj6}9 literal 0 HcmV?d00001 diff --git a/src/plugins/ukrainian/uaranobeclub.ts b/src/plugins/ukrainian/uaranobeclub.ts new file mode 100644 index 0000000..0a34af9 --- /dev/null +++ b/src/plugins/ukrainian/uaranobeclub.ts @@ -0,0 +1,275 @@ +import { fetchApi } from '@libs/fetch'; +import { Plugin } from '@typings/plugin'; + +const UA_RANOBE_ID = 'uaranobeclub' as const; +const UA_RANOBE_URL = 'https://uaranobe.club/' as const; + +type Writing = { + id: string; + title: string; + image: string; + slug: string; + description: string; + updatedAt: string; + episodes: Episode[]; + category: Category; + genres: GenreConnection[]; + __typename: string; + scanlators: { + scanlator: { + scanlatorName: string; + username: string; + episodes: Episode[]; + __typename: string; + }; + __typename: string; + }[]; +}; + +type Episode = { + seqTitle: string; + title: string; + slug: string; + subId: string; + text: string; + __typename: string; +}; + +type Category = { + id: string; + name: string; + __typename: string; +}; + +type GenreConnection = { + genreId: string; + genre: Genre; + __typename: string; +}; + +type Genre = { + id: string; + name: string; + __typename: string; +}; + +type WritingsResponse = { + writingsCount: number; + writings: Writing[]; +}; + +type WritingResponse = { + data: { + writingBySlug: Writing; + }; +}; + +type EpisodeResponse = { + data: { + episodeBySlug: Episode; + }; +}; + +class UaRanobeClubApi { + static async fetch( + query: string, + variables: TVariables, + ): Promise { + const response = await fetchApi(`${UA_RANOBE_URL}graphql`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + query, + variables, + }), + }); + + return (await response.json()) as TData; + } + + static async fetchChapter( + slug: string, + writingSlug: string, + ): Promise { + const query = ` + query EpisodeBySlug($slug: String!, $writingSlug: String!) { + episodeBySlug(slug: $slug, writingSlug: $writingSlug) { + text + } + } + `; + + const variables = { + slug, + writingSlug, + } as const; + + const response = await UaRanobeClubApi.fetch< + EpisodeResponse, + typeof variables + >(query, variables); + + return response.data.episodeBySlug.text; + } + + static async fetchNovel(slug: string): Promise { + const query = ` + query Writing($slug: String!) { + writingBySlug(slug: $slug) { + id + title + originalTitle + image + slug + description + scanlators { + scanlator { + scanlatorName + username + episodes(oldestFirst: false, slug: $slug) { + id + subId + seqTitle + title + slug + __typename + } + __typename + } + __typename + } + genres { + genreId + genre { + id + name + __typename + } + __typename + } + __typename + } + } + `; + + const variables = { slug }; + + const response = await UaRanobeClubApi.fetch< + WritingResponse, + typeof variables + >(query, variables); + + return response?.data?.writingBySlug ?? ''; + } + + static async fetchListWithNovel( + skip = 0, + search = '', + ): Promise { + const query = ` + query Writings($skip: Int!, $search: String) { + writingsCount(search: $search) + writings(skip: $skip, search: $search) { + id + title + image + slug + __typename + } + } + `; + const variables = { + skip, + search, + } as const; + + const response = await UaRanobeClubApi.fetch< + { data: WritingsResponse }, + typeof variables + >(query, variables); + + return response.data; + } +} + +class UaRanobeClub implements Plugin.PluginBase { + id = UA_RANOBE_ID; + name = 'UA Ranobe Club'; + site = UA_RANOBE_URL; + version = '1.1.4'; + icon = `src/uk/${this.id}/icon.png`; + + async popularNovels(page: number): Promise { + const skip = (page - 1) * 10; + + const data = await UaRanobeClubApi.fetchListWithNovel(skip, ''); + + const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map( + ({ title, image, slug }) => ({ + name: title, + cover: image, + path: slug, + }), + ); + + return novelItems; + } + + async parseNovel(novelPath: string): Promise { + const slug = novelPath.split(UA_RANOBE_URL).join(''); + + const data: Writing = await UaRanobeClubApi.fetchNovel(slug); + + const chapters: Plugin.ChapterItem[] = + data.scanlators?.[0]?.scanlator?.episodes?.map( + ({ title, seqTitle, slug: chapterSlug, subId }) => ({ + name: `${seqTitle}. ${title}`, + path: chapterSlug + `#${slug}`, + chapterNumber: parseInt(subId, 10), // Предполагаем, что subId это число + }), + ) || []; + + const sourceNovel: Plugin.SourceNovel = { + genres: data.genres?.map(({ genre }) => genre.name).join(',') || '', + chapters, + name: data.title, + path: data.slug, + summary: data.description, + cover: data.image, + }; + + return sourceNovel; + } + + async parseChapter(chapterPath: string): Promise { + const [slug, writingSlug] = chapterPath + .split(UA_RANOBE_URL) + .join('') + .split('#'); + const chapterText = await UaRanobeClubApi.fetchChapter(slug, writingSlug); + + return chapterText; + } + + async searchNovels( + searchTerm: string, + page: number, + ): Promise { + const skip = (page - 1) * 10; + + const data = await UaRanobeClubApi.fetchListWithNovel(skip, searchTerm); + const novelItems: Plugin.NovelItem[] = (data?.writings ?? []).map( + ({ title, image, slug }) => ({ + name: title, + cover: image, + path: slug, + }), + ); + + return novelItems; + } +} + +export default new UaRanobeClub();