/// /** * MIT License * Copyright (c) 2023 Yan */ import dayjs from 'dayjs'; import ComicDownloader from '../../core'; const API_HEADERS = { 'User-Agent': '"User-Agent" to "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44"', version: dayjs().format('YYYY.MM.DD'), platform: '1', region: '0', }; const API_URL = 'https://api.copymanga.org/'; export default class CopymangaDownloader extends ComicDownloader { static readonly siteName = 'copymanga'; static canHandleUrl(url: string): boolean { return /copymanga/.test(url); } static readonly preferredCLIPresets: Partial = { format: 'webp', }; static urlCompletion(shorthandUrl: string): string { return `https://www.copymanga.site/comic/${shorthandUrl}`; } constructor(protected destination: string, protected configs: Configs = {}) { super(destination, configs); this.axios.defaults.baseURL = API_URL; Object.keys(API_HEADERS).forEach(key => { this.axios.defaults.headers[key] = API_HEADERS[key as keyof typeof API_HEADERS] ?? ''; }); this.axios.defaults.headers.webp = configs.format === 'jpg' ? 0 : 1; } async getSegmentedChapters( mangaId: string, page: number, group = 'default', ): Promise { const res = await this.axios.get( `/api/v3/comic/${mangaId}/group/${group}/chapters?limit=500&offset=${ page * 500 }&platform=3`, ); return res?.data?.results?.list?.map(item => { const uri = `/api/v3/comic/${mangaId}/chapter/${item.uuid}?platform=3`; return { index: item.index, name: item.name, uri, }; }); } getMangaId(url: string) { return new URL(url).pathname.split('/').pop(); } async getSerieInfo( url: string, options: Partial = {}, ): Promise { const mangaId = this.getMangaId(url); if (!mangaId) { throw new Error('Invalid URL.'); } const res = await this.axios.get( `/api/v3/comic2/${mangaId}`, ); const data = res?.data?.results?.comic ?? {}; const count = res?.data?.results?.groups?.default?.count ?? 0; const title = data.name; const info: ComicInfo = { Manga: 'YesAndRightToLeft', Serie: title, Summary: data.brief, Location: data.region?.display, Count: count, Web: url, Status: data.status?.value === 0 ? 'Ongoing' : 'End', Penciller: data.author?.map(e => e.name)?.join(','), Tags: data.theme?.map(e => e.name)?.join(','), }; const chapters: Chapter[] = []; const pagination = 500; const pages = Math.ceil((count || 1) / pagination); for (let page = 0; page < pages; page += pagination) { const segment = await this.getSegmentedChapters( mangaId, page, options.group, ); chapters.push(...segment); } return { title, chapters, info }; } protected async getImageList(url: string): Promise<(string | null)[]> { const res = await this.axios.get(url); const data = res?.data?.results?.chapter; /** * Copymanga now messes up the order of images, * but in the API data, there's a chapter.words array that contains the correct page order. */ const imageUrls = data?.contents.map(item => item.url); const imageOrder = data?.words; const orderedPages: string[] = []; if (imageOrder?.length) { imageOrder.forEach((order, index) => { orderedPages[order] = imageUrls[index]; }); return orderedPages; } return imageUrls; } }