From b2ad2a20519bc65b77a7a65a3e7928cd4be7a2d5 Mon Sep 17 00:00:00 2001 From: xMohnad Date: Sun, 15 Jun 2025 03:13:40 +0300 Subject: [PATCH] Refactor Webtoons: format code, simplify logic with mangaFromElement --- javascript/manga/src/all/webtoons.js | 771 +++++++++++++++------------ 1 file changed, 423 insertions(+), 348 deletions(-) diff --git a/javascript/manga/src/all/webtoons.js b/javascript/manga/src/all/webtoons.js index ba5b5bf5..36534056 100644 --- a/javascript/manga/src/all/webtoons.js +++ b/javascript/manga/src/all/webtoons.js @@ -1,3 +1,4 @@ +// prettier-ignore const mangayomiSources = [{ "name": "Webtoons", "langs": ["en", "fr", "id", "th", "es", "zh", "de"], @@ -14,371 +15,445 @@ const mangayomiSources = [{ }]; class DefaultExtension extends MProvider { - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" + headers = { + "User-Agent": + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36", + }; + getHeaders(url) { + return { + Referer: this.source.baseUrl, }; - getHeaders(url) { - return { - Referer: this.source.baseUrl - } + } + + async getItem(url, selector) { + const res = await new Client().get(this.source.baseUrl + url, this.headers); + const doc = new Document(res.body); + const mangas = []; + const elements = doc.select(selector); + for (const element of elements) { + const linkElement = element.selectFirst("a"); + const imageElement = linkElement.selectFirst("img"); + const imageUrl = imageElement.attr("src"); + const name = element.selectFirst("p.subj").text; + const link = linkElement.attr("href"); + const genre = []; + if (element.selectFirst("p.genre").text === "") { + genre.push(element.selectFirst("span.genre").text); + } else { + genre.push(element.selectFirst("p.genre").text); + } + mangas.push({ + name: name, + imageUrl: imageUrl, + link: link, + genre: genre, + }); + } + return mangas; + } + + mangaFromElement(doc) { + const list = []; + for (const el of doc.select(`div.webtoon_list_wrap li a`)) { + const imageUrl = el.selectFirst("img").getSrc; + const name = el.selectFirst("strong.title").text; + const link = el.getHref; + list.push({ name, imageUrl, link }); } - async getItem(url, selector) { - const res = await new Client().get(this.source.baseUrl + url, this.headers); - const doc = new Document(res.body); - const mangas = []; - const elements = doc.select(selector); - for (const element of elements) { - const linkElement = element.selectFirst("a"); - const imageElement = linkElement.selectFirst("img"); - const imageUrl = imageElement.attr("src"); - const name = element.selectFirst("p.subj").text; - const link = linkElement.attr("href"); - const genre = []; - if (element.selectFirst("p.genre").text === "") { - genre.push(element.selectFirst("span.genre").text); - } else { - genre.push(element.selectFirst("p.genre").text); - } - mangas.push({ - name: name, - imageUrl: imageUrl, - link: link, - genre: genre - }); - } - return mangas; + return list; + } + + async getPopular(page) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get( + `${baseUrl}/${this.langCode()}/originals`, + ); + const doc = new Document(res.body); + + return { + list: this.mangaFromElement(doc), + hasNextPage: false, + }; + } + + async getLatestUpdates(page) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get( + `${baseUrl}/${this.langCode()}/originals?sortOrder=UPDATE`, + ); + const doc = new Document(res.body); + + return { + list: this.mangaFromElement(doc), + hasNextPage: false, + }; + } + + async search(query, page, filters) { + let keyword = query.trim().replace(/\s+/g, "+"); + let hasNextPage = true; + let type_originals = "WEBTOON"; + let type_canvas = "CHALLENGE"; + let fetch_originals = "ul.card_lst li"; + let fetch_canvas = "div.challenge_lst.search li"; + let list_originals = []; + let list_canvas = []; + let list = []; + + if (query !== "") { + list_originals = await this.getItem( + `/${this.langCode()}/search?keyword=${keyword}&searchType=` + + type_originals + + `&page=${page}`, + fetch_originals, + ); + list_canvas = await this.getItem( + `/${this.langCode()}/search?keyword=${keyword}&searchType=` + + type_canvas + + `&page=${page}`, + fetch_canvas, + ); + if (filters) { + } + list = list_originals.concat(list_canvas); + } else { } - async getPopular(page) { - const baseUrl = this.source.baseUrl; - const client = new Client(); - const res = await client.get(`${baseUrl}/${this.langCode()}/originals`); - const doc = new Document(res.body); - - const days = ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY", "SATURDAY", "SUNDAY"]; - let dayElements = []; - - for (const day of days) { - const elements = doc.select(`div.daily_section._list_${day} li`); - dayElements.push(elements); - } - - const maxElements = Math.max(...dayElements.map(elements => elements.length)); - let list = []; - for (let i = 0; i < maxElements; i++) { - for (let j = 0; j < days.length; j++) { - const elements = dayElements[j]; - if (i < elements.length) { - const element = elements[i]; - const linkElement = element.selectFirst("a"); - const imageElement = linkElement.selectFirst("img"); - const imageUrl = imageElement.attr("src"); - const name = element.selectFirst("p.subj").text; - const link = linkElement.attr("href"); - list.push({ name, imageUrl, link }); - } - } - } - - const completed = doc.select("div.daily_lst.comp li"); - for (const element of completed) { - const linkElement = element.selectFirst("a"); - const imageElement = linkElement.selectFirst("img"); - const imageUrl = imageElement.attr("src"); - const name = element.selectFirst("p.subj").text; - const link = linkElement.attr("href"); - list.push({ name, imageUrl, link }); - } - - return { - list: list, - hasNextPage: false - }; + if (list.length === 0) { + hasNextPage = false; } + return { + list: list, + hasNextPage: hasNextPage, + }; + } - async getLatestUpdates(page) { - const Day = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; - const list = await this.getItem(`/${this.langCode()}/originals?sortOrder=UPDATE`, "div.daily_section._list_" + Day[new Date().getDay()] + " li"); - return { - list: list, - hasNextPage: false - }; + async getDetail(url) { + const res = await new Client().get(url); + const doc = new Document(res.body); + const info = doc.selectFirst("div.cont_box"); + const cover = + info + .selectFirst("div.detail_body") + ?.attr("style") + ?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] ?? + info.selectFirst("span.thmb img")?.attr("src"); + const title = info.selectFirst("h1.subj, h3.subj").text.trim(); + const genre = + Array.from(info.select("p.genre")).map((el) => el.text) != "" + ? Array.from(info.select("p.genre")).map((el) => el.text) + : [info.selectFirst("div.info h2").text]; + const author = + info + .selectFirst("div.author_area") + .text.replace(/\s+/g, " ") + .replace(/author info/g, "") + .trim() ?? info.selectFirst("a.author").text; + const status_str = info.selectFirst("p.day_info").text; + var status; + if (status_str == "COMPLETED") { + status = 1; + } else { + status = 0; } + const desc = info.selectFirst("p.summary").text.replace(/\s+/g, " ").trim(); + const chapters = []; + let tester = ""; + let page = 1; - async search(query, page, filters) { - let keyword = query.trim().replace(/\s+/g, '+'); - let hasNextPage = true; - let type_originals = "WEBTOON"; - let type_canvas = "CHALLENGE"; - let fetch_originals = "ul.card_lst li"; - let fetch_canvas = "div.challenge_lst.search li"; - let list_originals = []; - let list_canvas = []; - let list = []; + while (tester !== "#1") { + const res = await new Client().get(url + `&page=${page}`); + const doc = new Document(res.body); + const info = doc.selectFirst("div.cont_box"); + const elements = info.select("div.detail_lst li"); - if (query !== "") { - list_originals = await this.getItem(`/${this.langCode()}/search?keyword=${keyword}&searchType=` + type_originals + `&page=${page}`, fetch_originals); - list_canvas = await this.getItem(`/${this.langCode()}/search?keyword=${keyword}&searchType=` + type_canvas + `&page=${page}`, fetch_canvas); - if (filters) { } - list = list_originals.concat(list_canvas); - } else { - - } - - if (list.length === 0) { hasNextPage = false; } - return { - list: list, - hasNextPage: hasNextPage - }; + for (const element of elements) { + tester = element.selectFirst("span.tx").text.trim(); + const dateString = element.selectFirst("span.date").text.trim(); + const date = new Date( + this.formatDateString(dateString, this.source.lang), + ); + const millisecondsSinceEpoch = date.getTime(); + const millisecondsString = millisecondsSinceEpoch.toString(); + chapters.push({ + name: tester + " " + element.selectFirst("span.subj span").text, + url: element.selectFirst("a").attr("href"), + dateUpload: millisecondsString, + }); + } + page++; } + return { + name: title, + link: url, + genre: genre, + description: desc, + imageUrl: cover, + author: author, + status: status, + episodes: chapters, + }; + } - async getDetail(url) { - const res = await new Client().get(url); - const doc = new Document(res.body); - const info = doc.selectFirst("div.cont_box"); - const cover = info.selectFirst("div.detail_body")?.attr("style")?.match(/url\(['"]?(.*?)['"]?\)/)?.[1] ?? info.selectFirst("span.thmb img")?.attr("src"); - const title = info.selectFirst("h1.subj, h3.subj").text.trim(); - const genre = Array.from(info.select("p.genre")).map(el => el.text) != '' ? Array.from(info.select("p.genre")).map(el => el.text) : [info.selectFirst("div.info h2").text]; - const author = info.selectFirst("div.author_area").text.replace(/\s+/g, ' ').replace(/author info/g, '').trim() ?? info.selectFirst("a.author").text; - const status_str = info.selectFirst("p.day_info").text; - var status; - if (status_str == "COMPLETED") { - status = 1; - } else { - status = 0; + langCode() { + return { + en: "en", + fr: "fr", + id: "id", + th: "th", + es: "es", + zh: "zh-hant", + de: "de", + }[this.source.lang]; + } + + formatDateString(dateStr, lang) { + // Month translations for supported languages + const monthTranslations = { + en: [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ], + fr: [ + "janv.", + "févr.", + "mars", + "avr.", + "mai", + "juin", + "juil.", + "août", + "sept.", + "oct.", + "nov.", + "déc.", + ], + id: [ + "Jan", + "Feb", + "Mar", + "Apr", + "Mei", + "Jun", + "Jul", + "Agt", + "Sep", + "Okt", + "Nov", + "Des", + ], + th: [ + "ม.ค.", + "ก.พ.", + "มี.ค.", + "เม.ย.", + "พ.ค.", + "มิ.ย.", + "ก.ค.", + "ส.ค.", + "ก.ย.", + "ต.ค.", + "พ.ย.", + "ธ.ค.", + ], + es: [ + "ene.", + "feb.", + "mar.", + "abr.", + "may.", + "jun.", + "jul.", + "ago.", + "sep.", + "oct.", + "nov.", + "dic.", + ], + zh: [], // No need for month names; uses yyyy年MM月dd日 format + de: [], // No need for month names; uses dd.MM.yyyy format + }; + const months = monthTranslations[lang]; + let parts; + let month; + let day; + let year; + // Handle formats based on the language + switch (lang) { + case "zh": + // Expected format: yyyy年MM月dd日 + const match = dateStr.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/); + year = match[1]; + month = match[2]; + day = match[3]; + case "de": + // Expected format: dd.MM.yyyy + parts = dateStr.split("."); + if (parts.length === 3) { + month = parts[1]; + day = parts[0]; + year = parts[2]; } - const desc = info.selectFirst("p.summary").text.replace(/\s+/g, ' ').trim(); - const chapters = []; - let tester = ""; - let page = 1; - - while (tester !== "#1") { - const res = await new Client().get(url + `&page=${page}`); - const doc = new Document(res.body); - const info = doc.selectFirst("div.cont_box"); - const elements = info.select("div.detail_lst li"); - - for (const element of elements) { - tester = element.selectFirst("span.tx").text.trim(); - const dateString = element.selectFirst("span.date").text.trim(); - const date = new Date(this.formatDateString(dateString, this.source.lang)); - const millisecondsSinceEpoch = date.getTime(); - const millisecondsString = millisecondsSinceEpoch.toString(); - chapters.push({ - name: tester + " " + element.selectFirst("span.subj span").text, - url: element.selectFirst('a').attr("href"), - dateUpload: millisecondsString - }); - } - page++; + case "es": + case "fr": + case "id": + case "th": + // Expected format: dd MMM yyyy + parts = dateStr.split(" "); + if (parts.length === 3) { + month = months.indexOf(parts[1]) + 1; + day = parts[0]; + year = parts[2]; + } + break; + default: + parts = dateStr.split(" "); + if (parts.length === 3) { + month = months.indexOf(parts[0]) + 1; + day = parts[1].replace(",", ""); + year = parts[2]; } - return { - name: title, - link: url, - genre: genre, - description: desc, - imageUrl: cover, - author: author, - status: status, - episodes: chapters - }; } - - - - langCode() { - return { - en: "en", - fr: "fr", - id: "id", - th: "th", - es: "es", - zh: "zh-hant", - de: "de" - }[this.source.lang]; + if (!month || !year || !day) { + return Date.now(); } + return `${year}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`; + } - formatDateString(dateStr, lang) { - // Month translations for supported languages - const monthTranslations = { - en: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], - fr: ["janv.", "févr.", "mars", "avr.", "mai", "juin", "juil.", "août", "sept.", "oct.", "nov.", "déc."], - id: ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agt", "Sep", "Okt", "Nov", "Des"], - th: ["ม.ค.", "ก.พ.", "มี.ค.", "เม.ย.", "พ.ค.", "มิ.ย.", "ก.ค.", "ส.ค.", "ก.ย.", "ต.ค.", "พ.ย.", "ธ.ค."], - es: ["ene.", "feb.", "mar.", "abr.", "may.", "jun.", "jul.", "ago.", "sep.", "oct.", "nov.", "dic."], - zh: [], // No need for month names; uses yyyy年MM月dd日 format - de: [] // No need for month names; uses dd.MM.yyyy format - }; - const months = monthTranslations[lang]; - let parts; - let month; - let day; - let year; - // Handle formats based on the language - switch (lang) { - case "zh": - // Expected format: yyyy年MM月dd日 - const match = dateStr.match(/(\d{4})年(\d{1,2})月(\d{1,2})日/); - year = match[1]; - month = match[2]; - day = match[3]; - case "de": - // Expected format: dd.MM.yyyy - parts = dateStr.split('.'); - if (parts.length === 3) { - month = parts[1]; - day = parts[0]; - year = parts[2]; - } - case "es": - case "fr": - case "id": - case "th": - // Expected format: dd MMM yyyy - parts = dateStr.split(' '); - if (parts.length === 3) { - month = months.indexOf(parts[1]) + 1; - day = parts[0]; - year = parts[2]; - } - break; - default: - parts = dateStr.split(' '); - if (parts.length === 3) { - month = months.indexOf(parts[0]) + 1; - day = parts[1].replace(',', ''); - year = parts[2]; - } - } - if (!month || !year || !day) { - return Date.now(); - } - return `${year}-${month.toString().padStart(2, '0')}-${day.toString().padStart(2, '0')}`; - - } - - async getPageList(url) { - const res = await new Client().get(url); - const doc = new Document(res.body); - const urls = []; - const imageElement = doc.selectFirst('div#_imageList'); - const img_urls = imageElement.select('img'); - for (let i = 0; i < img_urls.length; i++) { - urls.push(img_urls[i].attr("data-url")); - } - return urls; - } - - - - getFilterList() { - return [{ - type: "sort", - name: "Official or Challenge", - type_name: "SelectFilter", - values: [{ - type_name: "SelectOption", - name: "Any", - value: "any" - }, - { - type_name: "SelectOption", - name: "Official only", - value: "official" - }, - { - type_name: "SelectOption", - name: "Challenge only", - value: "challenge" - }] - }, - { - type: "categories", - name: "Genre", - type_name: "SelectFilter", - values: [{ - type_name: "SelectOption", - name: "All", - value: "" - }, - { - type_name: "SelectOption", - name: "Action", - value: "action" - }, - { - type_name: "SelectOption", - name: "Comedy", - value: "comedy" - }, - { - type_name: "SelectOption", - name: "Drama", - value: "drama" - }, - { - type_name: "SelectOption", - name: "Fantasy", - value: "fantasy" - }, - { - type_name: "SelectOption", - name: "Heartwarming", - value: "heartwarming" - }, - { - type_name: "SelectOption", - name: "Historical", - value: "historical" - }, - { - type_name: "SelectOption", - name: "Horror", - value: "horror" - }, - { - type_name: "SelectOption", - name: "Informative", - value: "tiptoon" - }, - { - type_name: "SelectOption", - name: "Mystery", - value: "mystery" - }, - { - type_name: "SelectOption", - name: "Romance", - value: "romance" - }, - { - type_name: "SelectOption", - name: "Sci-fi", - value: "sf" - }, - { - type_name: "SelectOption", - name: "Slice of life", - value: "slice_of_life" - }, - { - type_name: "SelectOption", - name: "Sports", - value: "sports" - }, - { - type_name: "SelectOption", - name: "Superhero", - value: "super_hero" - }, - { - type_name: "SelectOption", - name: "Supernatural", - value: "supernatural" - }] - } - ]; + async getPageList(url) { + const res = await new Client().get(url); + const doc = new Document(res.body); + const urls = []; + const imageElement = doc.selectFirst("div#_imageList"); + const img_urls = imageElement.select("img"); + for (let i = 0; i < img_urls.length; i++) { + urls.push(img_urls[i].attr("data-url")); } + return urls; + } + getFilterList() { + return [ + { + type: "sort", + name: "Official or Challenge", + type_name: "SelectFilter", + values: [ + { + type_name: "SelectOption", + name: "Any", + value: "any", + }, + { + type_name: "SelectOption", + name: "Official only", + value: "official", + }, + { + type_name: "SelectOption", + name: "Challenge only", + value: "challenge", + }, + ], + }, + { + type: "categories", + name: "Genre", + type_name: "SelectFilter", + values: [ + { + type_name: "SelectOption", + name: "All", + value: "", + }, + { + type_name: "SelectOption", + name: "Action", + value: "action", + }, + { + type_name: "SelectOption", + name: "Comedy", + value: "comedy", + }, + { + type_name: "SelectOption", + name: "Drama", + value: "drama", + }, + { + type_name: "SelectOption", + name: "Fantasy", + value: "fantasy", + }, + { + type_name: "SelectOption", + name: "Heartwarming", + value: "heartwarming", + }, + { + type_name: "SelectOption", + name: "Historical", + value: "historical", + }, + { + type_name: "SelectOption", + name: "Horror", + value: "horror", + }, + { + type_name: "SelectOption", + name: "Informative", + value: "tiptoon", + }, + { + type_name: "SelectOption", + name: "Mystery", + value: "mystery", + }, + { + type_name: "SelectOption", + name: "Romance", + value: "romance", + }, + { + type_name: "SelectOption", + name: "Sci-fi", + value: "sf", + }, + { + type_name: "SelectOption", + name: "Slice of life", + value: "slice_of_life", + }, + { + type_name: "SelectOption", + name: "Sports", + value: "sports", + }, + { + type_name: "SelectOption", + name: "Superhero", + value: "super_hero", + }, + { + type_name: "SelectOption", + name: "Supernatural", + value: "supernatural", + }, + ], + }, + ]; + } }