From 32081cc413a1a3fff6b9039b75016396f4161219 Mon Sep 17 00:00:00 2001 From: xMohnad Date: Sat, 5 Apr 2025 04:03:19 +0300 Subject: [PATCH 1/5] "manga(teamx): Add TeamX source" --- javascript/manga/src/ar/teamx.js | 374 +++++++++++++++++++++++++++++++ 1 file changed, 374 insertions(+) create mode 100644 javascript/manga/src/ar/teamx.js diff --git a/javascript/manga/src/ar/teamx.js b/javascript/manga/src/ar/teamx.js new file mode 100644 index 00000000..24fe5e7e --- /dev/null +++ b/javascript/manga/src/ar/teamx.js @@ -0,0 +1,374 @@ +const mangayomiSources = [ + { + name: "TeamX", + lang: "ar", + baseUrl: "https://olympustaff.com", + apiUrl: "", + iconUrl: + "https://www.google.com/s2/favicons?sz=256&domain=https://olympustaff.com&size=256", + typeSource: "single", + itemType: 0, + version: "1.0.0", + pkgPath: "manga/src/ar/teamx.js", + }, +]; + +class DefaultExtension extends MProvider { + constructor() { + super(); + this.client = new Client(); + this.baseUrl = new SharedPreferences().get("overrideBaseUrl1"); + } + + getHeaders(url) { + return { + Referer: this.source.baseUrl, + }; + } + + toStatus(status) { + return ( + { + مستمرة: 0, + مكتملة: 1, + متوقف: 2, + متروك: 3, + مكتمل: 4, + }[status] ?? 5 + ); + } + hasNextPage(doc) { + return ( + doc.selectFirst(".pagination li.page-item a[rel='next'] ").attr("href") != + "" + ); + } + + parseChapterDate(date) { + // Format YYYY-MM-DD + return String(new Date(date).toISOString().split("T")[0]); + } + + async request(slug) { + const res = await this.client.get(`${this.baseUrl}${slug}`); + return new Document(res.body); + } + + chapterFromElement(element) { + const chapter = { + name: "", + dateUpload: 0, + url: "", + }; + + const chpNum = element.selectFirst("div.epl-num").text.trim(); + const chpTitle = element.selectFirst("div.epl-title").text.trim(); + + chapter.name = chpTitle.includes(chpNum.replace(/[^0-9]/g, "")) + ? chpTitle + : !chpNum + ? chpTitle + : !chpTitle + ? chpNum + : `${chpNum} - ${chpTitle}`; + + chapter.dateUpload = this.parseChapterDate( + element.selectFirst("div.epl-date").text.trim(), + ); + chapter.url = element.getHref; + + return chapter; + } + async chapterListParse(response) { + const allElements = []; + let doc = response; + + while (true) { + const pageChapters = doc.select("div.eplister ul a"); + if (pageChapters.length === 0) { + break; + } + + allElements.push(...pageChapters); + const nextPage = doc.select("a[rel=next]"); + if (!nextPage.length > 0) { + break; + } + + const nextUrl = nextPage.at(0).attr("href"); + const nextResponse = await new Client().get(nextUrl); + doc = new Document(nextResponse.body); + } + + return allElements.map((element) => this.chapterFromElement(element)); + } + async getMangaList(slug) { + const doc = await this.request(`/${slug}`); + const mangaElements = doc.select(".listupd .bsx"); + + const list = []; + for (const element of mangaElements) { + const name = element.selectFirst("a").attr("title")?.trim(); + const imageUrl = element.selectFirst("img").getSrc; + const link = element.getHref; + list.push({ name, imageUrl, link }); + } + const hasNextPage = this.hasNextPage(doc); + return { list: list, hasNextPage }; + } + + async getPopular(page) { + return await this.getMangaList(`series?page=${page}`); + } + + async getLatestUpdates(page) { + const doc = await this.request(`/?page=${page}`); + const mangaElements = doc.select(".post-body .box"); + const list = []; + for (const element of mangaElements) { + const name = element.selectFirst(".info a h3").text; + const imageUrl = element.selectFirst(".imgu img").getSrc; + const link = element.selectFirst(".imgu a").getHref; + list.push({ name, imageUrl, link }); + } + const hasNextPage = this.hasNextPage(doc); + return { list: list, hasNextPage }; + } + + get supportsLatest() { + throw new Error("supportsLatest not implemented"); + } + + async search(query, page, filters) { + if (!query) { + const type = filters[0].values[filters[0].state].value; + const status = filters[1].values[filters[1].state].value; + const genre = filters[2].values[filters[2].state].value; + return await this.getMangaList( + `series?page=${page}&genre=${genre}&type=${type}&status=${status}`, + ); + } + + const doc = await this.request(`/ajax/search?keyword="${query}`).select( + "li.list-group-item", + ); + + const list = []; + for (const element of doc) { + const name = element.selectFirst("div.ms-2 a").text; + const imageUrl = element.selectFirst("a img").getSrc; + const link = element.selectFirst("div.ms-2 a").getHref; + list.push({ name, imageUrl, link }); + } + + return { list, hasNextPage: false }; + } + + async getDetail(url) { + const baseUrl = new SharedPreferences().get("overrideBaseUrl1"); + const res = await this.client.get(url); + const doc = new Document(res.body); + + const title = doc.selectFirst("div.author-info-title h1")?.text.trim(); + const imageUrl = doc.selectFirst("img.shadow-sm")?.getSrc; + const description = doc.selectFirst(".review-content > p")?.text.trim(); + const authorText = doc + .selectFirst( + ".full-list-info > small:first-child:contains(الرسام) + small", + ) + ?.text?.trim(); + const author = authorText !== "غير معروف" ? authorText : null; + const status = this.toStatus( + doc + .selectFirst( + ".full-list-info > small:first-child:contains(الحالة) + small", + ) + ?.text?.trim(), + ); + const genre = doc + .select("div.review-author-info a") + .map((e) => e.text.trim()); + + const chapters = await this.chapterListParse(doc); + + return { + title, + imageUrl, + description, + author, + status, + genre, + chapters, + }; + } + + // For novel html content + async getHtmlContent(url) { + throw new Error("getHtmlContent not implemented"); + } + // Clean html up for reader + async cleanHtmlContent(html) { + throw new Error("cleanHtmlContent not implemented"); + } + // For anime episode video list + async getVideoList(url) { + throw new Error("getVideoList not implemented"); + } + + // For manga chapter pages + async getPageList(url) { + const res = await this.client.get(url); + const doc = new Document(res.body); + + return doc.select("div.image_list img[src]").map((x) => ({ + url: x.attr("src"), + })); + } + + getFilterList() { + return [ + { + type_name: "SelectFilter", + name: "النوع", + values: [ + ["اختر النوع", ""], + ["مانها صيني", "مانها صيني"], + ["مانجا ياباني", "مانجا ياباني"], + ["ويب تون انجليزية", "ويب تون انجليزية"], + ["مانهوا كورية", "مانهوا كورية"], + ["ويب تون يابانية", "ويب تون يابانية"], + ["عربي", "عربي"], + ].map((x) => ({ + type_name: "SelectOption", + name: x[0], + value: x[1], + })), + }, + { + type_name: "SelectFilter", + name: "الحالة", + values: [ + ["اختر الحالة", ""], + ["مستمرة", "مستمرة"], + ["متوقف", "متوقف"], + ["مكتمل", "مكتمل"], + ["قادم قريبًا", "قادم قريبًا"], + ["متروك", "متروك"], + ].map((x) => ({ + type_name: "SelectOption", + name: x[0], + value: x[1], + })), + }, + { + type_name: "SelectFilter", + name: "التصنيف", + values: [ + ["اختر التصنيف", ""], + ["أكشن", "أكشن"], + ["إثارة", "إثارة"], + ["إيسيكاي", "إيسيكاي"], + ["بطل غير إعتيادي", "بطل غير إعتيادي"], + ["خيال", "خيال"], + ["دموي", "دموي"], + ["نظام", "نظام"], + ["صقل", "صقل"], + ["قوة خارقة", "قوة خارقة"], + ["فنون قتال", "فنون قتال"], + ["غموض", "غموض"], + ["وحوش", "وحوش"], + ["شونين", "شونين"], + ["حريم", "حريم"], + ["خيال علمي", "خيال علمي"], + ["مغامرات", "مغامرات"], + ["دراما", "دراما"], + ["خارق للطبيعة", "خارق للطبيعة"], + ["سحر", "سحر"], + ["كوميدي", "كوميدي"], + ["ويب تون", "ويب تون"], + ["زمكاني", "زمكاني"], + ["رومانسي", "رومانسي"], + ["شياطين", "شياطين"], + ["فانتازيا", "فانتازيا"], + ["عنف", "عنف"], + ["ملائكة", "ملائكة"], + ["بعد الكارثة", "بعد الكارثة"], + ["إعادة إحياء", "إعادة إحياء"], + ["اعمار", "اعمار"], + ["ثأر", "ثأر"], + ["زنزانات", "زنزانات"], + ["تاريخي", "تاريخي"], + ["حرب", "حرب"], + ["خارق", "خارق"], + ["سنين", "سنين"], + ["عسكري", "عسكري"], + ["بوليسي", "بوليسي"], + ["حياة مدرسية", "حياة مدرسية"], + ["واقع افتراضي", "واقع افتراضي"], + ["داخل لعبة", "داخل لعبة"], + ["داخل رواية", "داخل رواية"], + ["الحياة اليومية", "الحياة اليومية"], + ["رعب", "رعب"], + ["طبخ", "طبخ"], + ["مدرسي", "مدرسي"], + ["زومبي", "زومبي"], + ["شوجو", "شوجو"], + ["معالج", "معالج"], + ["شريحة من الحياة", "شريحة من الحياة"], + ["نفسي", "نفسي"], + ["تاريخ", "تاريخ"], + ["أكاديمية", "أكاديمية"], + ["أرواح", "أرواح"], + ["تراجيدي", "تراجيدي"], + ["ابراج", "ابراج"], + ["رياضي", "رياضي"], + ["مصاص دماء", "مصاص دماء"], + ["طبي", "طبي"], + ["مأساة", "مأساة"], + ["إيتشي", "إيتشي"], + ["انتقام", "انتقام"], + ["جوسي", "جوسي"], + ["موريم", "موريم"], + ["لعبة فيديو", "لعبة فيديو"], + ["مغني", "مغني"], + ["تشويق", "تشويق"], + ["نجاة", "نجاة"], + ["الجانب المظلم من الحياة", "الجانب المظلم من الحياة"], + ["سينين", "سينين"], + ["تنمر", "تنمر"], + ["حيوانات أليفة", "حيوانات أليفة"], + ["شرطة", "شرطة"], + ["الخيال العلمي", "الخيال العلمي"], + ["حشرات", "حشرات"], + ["عوالم", "عوالم"], + ["ممالك", "ممالك"], + ["مؤامرات", "مؤامرات"], + ["تخطيط", "تخطيط"], + ["سفر عبر الأبعاد", "سفر عبر الأبعاد"], + ["جواسيس", "جواسيس"], + ["بطل مخطط", "بطل مخطط"], + ["ممثل", "ممثل"], + ].map((x) => ({ + type_name: "SelectOption", + name: x[0], + value: x[1], + })), + }, + ]; + } + + getSourcePreferences() { + return [ + { + key: "overrideBaseUrl1", + editTextPreference: { + title: "Override BaseUrl", + summary: "https://olympustaff.com", + value: "https://olympustaff.com", + dialogTitle: "Override BaseUrl", + dialogMessage: "", + }, + }, + ]; + } +} From 1517c075cb2a2cbdbffc5be183980d29ae769409 Mon Sep 17 00:00:00 2001 From: xMohnad Date: Sat, 5 Apr 2025 07:58:22 +0000 Subject: [PATCH 2/5] manga(teamx): fix status values --- javascript/manga/src/ar/teamx.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/javascript/manga/src/ar/teamx.js b/javascript/manga/src/ar/teamx.js index 24fe5e7e..6f922c9d 100644 --- a/javascript/manga/src/ar/teamx.js +++ b/javascript/manga/src/ar/teamx.js @@ -31,10 +31,11 @@ class DefaultExtension extends MProvider { { مستمرة: 0, مكتملة: 1, + مكتمل: 1, متوقف: 2, متروك: 3, - مكتمل: 4, - }[status] ?? 5 + "قادم قريبًا": 4, + }[status] ?? 5 // 5 => unknown ); } hasNextPage(doc) { From d6a7af2d0eded334be49bc81748c912a9e814d2d Mon Sep 17 00:00:00 2001 From: xMohnad Date: Sat, 5 Apr 2025 11:12:04 +0000 Subject: [PATCH 3/5] manga(teamx): refactor and clean up code --- javascript/manga/src/ar/teamx.js | 188 +++++++++++++++---------------- 1 file changed, 91 insertions(+), 97 deletions(-) diff --git a/javascript/manga/src/ar/teamx.js b/javascript/manga/src/ar/teamx.js index 6f922c9d..d6e3c717 100644 --- a/javascript/manga/src/ar/teamx.js +++ b/javascript/manga/src/ar/teamx.js @@ -4,8 +4,9 @@ const mangayomiSources = [ lang: "ar", baseUrl: "https://olympustaff.com", apiUrl: "", - iconUrl: - "https://www.google.com/s2/favicons?sz=256&domain=https://olympustaff.com&size=256", + get iconUrl() { + return `https://www.google.com/s2/favicons?sz=256&domain=${this.baseUrl}`; + }, typeSource: "single", itemType: 0, version: "1.0.0", @@ -20,10 +21,10 @@ class DefaultExtension extends MProvider { this.baseUrl = new SharedPreferences().get("overrideBaseUrl1"); } + // Helper Methods + getHeaders(url) { - return { - Referer: this.source.baseUrl, - }; + return { Referer: this.source.baseUrl }; } toStatus(status) { @@ -38,16 +39,17 @@ class DefaultExtension extends MProvider { }[status] ?? 5 // 5 => unknown ); } + hasNextPage(doc) { return ( - doc.selectFirst(".pagination li.page-item a[rel='next'] ").attr("href") != - "" + doc + .selectFirst(".pagination li.page-item a[rel='next']") + ?.attr("href") !== "" ); } parseChapterDate(date) { - // Format YYYY-MM-DD - return String(new Date(date).toISOString().split("T")[0]); + return new Date(date).toISOString().split("T")[0]; } async request(slug) { @@ -55,130 +57,114 @@ class DefaultExtension extends MProvider { return new Document(res.body); } + // Chapters chapterFromElement(element) { - const chapter = { - name: "", - dateUpload: 0, - url: "", + const chpNum = element.selectFirst("div.epl-num")?.text.trim(); + const chpTitle = element.selectFirst("div.epl-title")?.text.trim(); + + let name; + if (chpTitle?.includes(chpNum?.replace(/[^0-9]/g, ""))) { + name = chpTitle; + } else if (!chpNum) { + name = chpTitle; + } else if (!chpTitle) { + name = chpNum; + } else { + name = `${chpNum} - ${chpTitle}`; + } + + return { + name, + dateUpload: this.parseChapterDate( + element.selectFirst("div.epl-date")?.text.trim(), + ), + url: element.getHref, }; - - const chpNum = element.selectFirst("div.epl-num").text.trim(); - const chpTitle = element.selectFirst("div.epl-title").text.trim(); - - chapter.name = chpTitle.includes(chpNum.replace(/[^0-9]/g, "")) - ? chpTitle - : !chpNum - ? chpTitle - : !chpTitle - ? chpNum - : `${chpNum} - ${chpTitle}`; - - chapter.dateUpload = this.parseChapterDate( - element.selectFirst("div.epl-date").text.trim(), - ); - chapter.url = element.getHref; - - return chapter; } + async chapterListParse(response) { const allElements = []; let doc = response; while (true) { const pageChapters = doc.select("div.eplister ul a"); - if (pageChapters.length === 0) { - break; - } + if (pageChapters.length === 0) break; allElements.push(...pageChapters); const nextPage = doc.select("a[rel=next]"); - if (!nextPage.length > 0) { - break; - } + if (nextPage.length === 0) break; - const nextUrl = nextPage.at(0).attr("href"); - const nextResponse = await new Client().get(nextUrl); + const nextUrl = nextPage[0].attr("href"); + const nextResponse = await this.client.get(nextUrl); doc = new Document(nextResponse.body); } return allElements.map((element) => this.chapterFromElement(element)); } + + // Manga Listing async getMangaList(slug) { const doc = await this.request(`/${slug}`); - const mangaElements = doc.select(".listupd .bsx"); + const list = doc.select(".listupd .bsx").map((element) => ({ + name: element.selectFirst("a")?.attr("title")?.trim(), + imageUrl: element.selectFirst("img")?.getSrc, + link: element.getHref, + })); - const list = []; - for (const element of mangaElements) { - const name = element.selectFirst("a").attr("title")?.trim(); - const imageUrl = element.selectFirst("img").getSrc; - const link = element.getHref; - list.push({ name, imageUrl, link }); - } - const hasNextPage = this.hasNextPage(doc); - return { list: list, hasNextPage }; + return { list, hasNextPage: this.hasNextPage(doc) }; } async getPopular(page) { - return await this.getMangaList(`series?page=${page}`); + return this.getMangaList(`series?page=${page}`); } async getLatestUpdates(page) { const doc = await this.request(`/?page=${page}`); - const mangaElements = doc.select(".post-body .box"); - const list = []; - for (const element of mangaElements) { - const name = element.selectFirst(".info a h3").text; - const imageUrl = element.selectFirst(".imgu img").getSrc; - const link = element.selectFirst(".imgu a").getHref; - list.push({ name, imageUrl, link }); - } - const hasNextPage = this.hasNextPage(doc); - return { list: list, hasNextPage }; - } - - get supportsLatest() { - throw new Error("supportsLatest not implemented"); + const list = doc.select(".post-body .box").map((element) => ({ + name: element.selectFirst(".info a h3")?.text, + imageUrl: element.selectFirst(".imgu img")?.getSrc, + link: element.selectFirst(".imgu a")?.getHref, + })); + + return { list, hasNextPage: this.hasNextPage(doc) }; } + // Search async search(query, page, filters) { if (!query) { - const type = filters[0].values[filters[0].state].value; - const status = filters[1].values[filters[1].state].value; - const genre = filters[2].values[filters[2].state].value; - return await this.getMangaList( + const [type, status, genre] = filters.map( + (filter, i) => filter.values[filters[i].state]?.value, + ); + return this.getMangaList( `series?page=${page}&genre=${genre}&type=${type}&status=${status}`, ); } - const doc = await this.request(`/ajax/search?keyword="${query}`).select( - "li.list-group-item", - ); - - const list = []; - for (const element of doc) { - const name = element.selectFirst("div.ms-2 a").text; - const imageUrl = element.selectFirst("a img").getSrc; - const link = element.selectFirst("div.ms-2 a").getHref; - list.push({ name, imageUrl, link }); - } + const doc = await this.request(`/ajax/search?keyword="${query}`); + const list = doc.select("li.list-group-item").map((element) => ({ + name: element.selectFirst("div.ms-2 a")?.text, + imageUrl: element.selectFirst("a img")?.getSrc, + link: element.selectFirst("div.ms-2 a")?.getHref, + })); return { list, hasNextPage: false }; } + // Detail async getDetail(url) { - const baseUrl = new SharedPreferences().get("overrideBaseUrl1"); const res = await this.client.get(url); const doc = new Document(res.body); const title = doc.selectFirst("div.author-info-title h1")?.text.trim(); const imageUrl = doc.selectFirst("img.shadow-sm")?.getSrc; const description = doc.selectFirst(".review-content > p")?.text.trim(); - const authorText = doc + + const author = doc .selectFirst( ".full-list-info > small:first-child:contains(الرسام) + small", ) ?.text?.trim(); - const author = authorText !== "غير معروف" ? authorText : null; + const status = this.toStatus( doc .selectFirst( @@ -186,6 +172,7 @@ class DefaultExtension extends MProvider { ) ?.text?.trim(), ); + const genre = doc .select("div.review-author-info a") .map((e) => e.text.trim()); @@ -196,27 +183,14 @@ class DefaultExtension extends MProvider { title, imageUrl, description, - author, + author: author && author !== "غير معروف" ? author : null, status, genre, chapters, }; } - // For novel html content - async getHtmlContent(url) { - throw new Error("getHtmlContent not implemented"); - } - // Clean html up for reader - async cleanHtmlContent(html) { - throw new Error("cleanHtmlContent not implemented"); - } - // For anime episode video list - async getVideoList(url) { - throw new Error("getVideoList not implemented"); - } - - // For manga chapter pages + // chapter pages async getPageList(url) { const res = await this.client.get(url); const doc = new Document(res.body); @@ -226,6 +200,7 @@ class DefaultExtension extends MProvider { })); } + // Filter getFilterList() { return [ { @@ -358,6 +333,7 @@ class DefaultExtension extends MProvider { ]; } + // Preferences getSourcePreferences() { return [ { @@ -372,4 +348,22 @@ class DefaultExtension extends MProvider { }, ]; } + + // Unimplemented Methods + get supportsLatest() { + throw new Error("Method not implemented: supportsLatest"); + } + + async getHtmlContent(url) { + throw new Error("Method not implemented: getHtmlContent"); + } + + async cleanHtmlContent(html) { + throw new Error("Method not implemented: cleanHtmlContent"); + } + + // For anime episode video list + async getVideoList(url) { + throw new Error("Method not implemented: getVideoList"); + } } From 5ca85d29b7fe8d393582afd61ae13ab33953840a Mon Sep 17 00:00:00 2001 From: xMohnad Date: Sun, 6 Apr 2025 20:11:52 +0300 Subject: [PATCH 4/5] manga(teamx): Fix URL encode query parameter --- javascript/manga/src/ar/teamx.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/javascript/manga/src/ar/teamx.js b/javascript/manga/src/ar/teamx.js index d6e3c717..8feefb51 100644 --- a/javascript/manga/src/ar/teamx.js +++ b/javascript/manga/src/ar/teamx.js @@ -140,7 +140,7 @@ class DefaultExtension extends MProvider { ); } - const doc = await this.request(`/ajax/search?keyword="${query}`); + const doc = await this.request(`/ajax/search?keyword=${query}`); const list = doc.select("li.list-group-item").map((element) => ({ name: element.selectFirst("div.ms-2 a")?.text, imageUrl: element.selectFirst("a img")?.getSrc, From f97436e2081b28dc24031b93e6e224bf9fd426cb Mon Sep 17 00:00:00 2001 From: xMohnad Date: Tue, 8 Apr 2025 10:32:50 +0000 Subject: [PATCH 5/5] manga(teamx): replace iconUrl getter with static value --- javascript/manga/src/ar/teamx.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/javascript/manga/src/ar/teamx.js b/javascript/manga/src/ar/teamx.js index 8feefb51..1404a6d9 100644 --- a/javascript/manga/src/ar/teamx.js +++ b/javascript/manga/src/ar/teamx.js @@ -4,12 +4,11 @@ const mangayomiSources = [ lang: "ar", baseUrl: "https://olympustaff.com", apiUrl: "", - get iconUrl() { - return `https://www.google.com/s2/favicons?sz=256&domain=${this.baseUrl}`; - }, + iconUrl: + "https://www.google.com/s2/favicons?sz=256&domain=https://olympustaff.com", typeSource: "single", itemType: 0, - version: "1.0.0", + version: "1.0.1", pkgPath: "manga/src/ar/teamx.js", }, ];