diff --git a/public/static/src/uk/uaranobeclub/icon.png b/public/static/src/uk/uaranobeclub/icon.png new file mode 100644 index 0000000..f7bedad Binary files /dev/null and b/public/static/src/uk/uaranobeclub/icon.png differ 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();