From 45a88930c3713ad4fd163646066a19c0ab06d6d8 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Sat, 19 Apr 2025 23:08:52 +0530 Subject: [PATCH 1/4] manga(readcomiconline): Added popular, latest and search --- javascript/manga/src/en/readcomiconline.js | 172 +++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 javascript/manga/src/en/readcomiconline.js diff --git a/javascript/manga/src/en/readcomiconline.js b/javascript/manga/src/en/readcomiconline.js new file mode 100644 index 00000000..1ecadc8a --- /dev/null +++ b/javascript/manga/src/en/readcomiconline.js @@ -0,0 +1,172 @@ +const mangayomiSources = [{ + "name": "ReadComicOnline", + "lang": "en", + "baseUrl": "https://readcomiconline.li", + "apiUrl": "", + "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/", + "typeSource": "single", + "itemType": 0, + "version": "0.0.1", + "pkgPath": "" +}]; + +class DefaultExtension extends MProvider { + constructor() { + super(); + this.client = new Client(); + } + + getHeaders() { + return { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6832.64 Safari/537.36", + "Referer": this.source.baseUrl, + "Origin": this.source.baseUrl, + } + } + + async request(slug) { + var url = slug + var baseUrl = this.source.baseUrl + if (!slug.includes(baseUrl)) url = baseUrl + slug; + var res = await this.client.get(url, this.getHeaders()); + return new Document(res.body); + } + + async getListPage(slug, page) { + var url = `${slug}page=${page}` + var doc = await this.request(url); + var baseUrl = this.source.baseUrl + var list = [] + + var comicList = doc.select(".list-comic > .item") + comicList.forEach(item => { + var name = item.selectFirst(".title").text; + var link = item.selectFirst("a").getHref + var imageSlug = item.selectFirst("img").getSrc + var imageUrl = `${baseUrl}${imageSlug}`; + list.push({ name, link, imageUrl }); + }); + + var pager = doc.select("ul.pager > li") + + var hasNextPage = false + if (pager.length > 0) hasNextPage = pager[pager.length - 1].text.includes("Last") ? true : false; + + return { list, hasNextPage } + + } + + async getPopular(page) { + return await this.getListPage("/ComicList/MostPopular?", page) + } + get supportsLatest() { + throw new Error("supportsLatest not implemented"); + } + async getLatestUpdates(page) { + return await this.getListPage("/ComicList/LatestUpdate?", page) + } + async search(query, page, filters) { + function getFilter(state) { + var rd = "" + state.forEach(item => { + if (item.state) { + rd += `${item.value},` + } + }) + return rd.slice(0, -1) + } + + var genre = getFilter(filters[0].state) + var status = filters[1].values[filters[1].state].value + var year = filters[2].values[filters[2].state].value + + + var slug = `/AdvanceSearch?comicName=${query}&ig=${encodeURIComponent(genre)}&status=${status}&pubDate=${year}&` + + return await this.getListPage(slug, page) + } + + async getDetail(url) { + throw new Error("getDetail not implemented"); + } + // 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) { + throw new Error("getPageList not implemented"); + } + getFilterList() { + function formateState(type_name, items, values) { + var state = []; + for (var i = 0; i < items.length; i++) { + state.push({ type_name: type_name, name: items[i], value: values[i] }) + } + return state; + } + + var filters = []; + + // Genre + var items = [ + "Action", "Adventure", "Anthology", "Anthropomorphic", "Biography", "Children", "Comedy", + "Crime", "Drama", "Family", "Fantasy", "Fighting", "Graphic Novels", "Historical", "Horror", + "Leading Ladies", "LGBTQ", "Literature", "Manga", "Martial Arts", "Mature", "Military", + "Mini-Series", "Movies & TV", "Music", "Mystery", "Mythology", "Personal", "Political", + "Post-Apocalyptic", "Psychological", "Pulp", "Religious", "Robots", "Romance", "School Life", + "Sci-Fi", "Slice of Life", "Sport", "Spy", "Superhero", "Supernatural", "Suspense", "Teen", + "Thriller", "Vampires", "Video Games", "War", "Western", "Zombies" + ]; + + var values = [ + "1", "2", "38", "46", "41", "49", "3", + "17", "19", "25", "20", "31", "5", "28", "15", + "35", "51", "44", "40", "4", "8", "33", + "56", "47", "55", "23", "21", "48", "42", + "43", "27", "39", "53", "9", "32", "52", + "16", "50", "54", "30", "22", "24", "29", "57", + "18", "34", "37", "26", "45", "36" + ]; + filters.push({ + type_name: "GroupFilter", + name: "Genres", + state: formateState("CheckBox", items, values) + }) + + // Status + items = ["Any", "Ongoing", "Completed"]; + values = ["", "Ongoing", "Completed"]; + filters.push({ + type_name: "SelectFilter", + name: "Status", + state: 0, + values: formateState("SelectOption", items, values) + }) + + // Years + const currentYear = new Date().getFullYear(); + items = Array.from({ length: currentYear - 1919 }, (_, i) => (1920 + i).toString()).reverse() + items = ["All", ...items] + values = ["", ...items] + filters.push({ + type_name: "SelectFilter", + name: "Year", + state: 0, + values: formateState("SelectOption", items, values) + }) + + return filters; + } + getSourcePreferences() { + throw new Error("getSourcePreferences not implemented"); + } +} From 678bc05fbde1b10c663a5fc04c4292540b611131 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Sun, 20 Apr 2025 00:18:03 +0530 Subject: [PATCH 2/4] manga(readcomiconline): Added details and fix image bug --- javascript/manga/src/en/readcomiconline.js | 61 ++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/javascript/manga/src/en/readcomiconline.js b/javascript/manga/src/en/readcomiconline.js index 1ecadc8a..fd941cc5 100644 --- a/javascript/manga/src/en/readcomiconline.js +++ b/javascript/manga/src/en/readcomiconline.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/", "typeSource": "single", "itemType": 0, - "version": "0.0.1", + "version": "0.0.2", "pkgPath": "" }]; @@ -43,7 +43,7 @@ class DefaultExtension extends MProvider { var name = item.selectFirst(".title").text; var link = item.selectFirst("a").getHref var imageSlug = item.selectFirst("img").getSrc - var imageUrl = `${baseUrl}${imageSlug}`; + var imageUrl = imageSlug.includes("http") ? imageSlug : `${baseUrl}${imageSlug}`; list.push({ name, link, imageUrl }); }); @@ -87,7 +87,62 @@ class DefaultExtension extends MProvider { } async getDetail(url) { - throw new Error("getDetail not implemented"); + function statusCode(status) { + return { + "Ongoing": 0, + "Completed": 1, + }[status] ?? 5; + } + var link = this.source.baseUrl + url + var doc = await this.request(url) + + var detailsSection = doc.selectFirst(".barContent") + var name = detailsSection.selectFirst("a").text + var imageSlug = doc.selectFirst(".rightBox").selectFirst("img").getSrc + var imageUrl = imageSlug.includes("http") ? imageSlug : `${this.source.baseUrl}${imageSlug}`; + var pTag = detailsSection.select("p") + + var description = pTag[pTag.length - 2].text + + var status = 5 + var genre = [] + var author = "" + var artist = "" + + pTag.forEach(p => { + var itemText = p.text.trim() + + if (itemText.includes("Genres")) { + genre = itemText.replace("Genres:", "").trim().split(", ") + } else if (itemText.includes("Status")) { + var sts = itemText.replace("Status: ", "").trim().split("\n")[0] + status = statusCode(sts) + } else if (itemText.includes("Writer")) { + author = itemText.replace("Writer: ", "") + } else if (itemText.includes("Artist")) { + artist = itemText.replace("Artist: ", "") + } + + }) + var chapters = [] + var tr = doc.selectFirst("table").select("tr") + tr.splice(0, 2) // 1st item in the table is headers & 2nd item is a line break + tr.forEach(item => { + var tds = item.select("td") + var aTag = tds[0].selectFirst("a") + var chapLink = aTag.getHref + var chapTitle = aTag.text.trim().replace(`${name} `, "") + var uploadDate = tds[1].text.trim() + var date = new Date(uploadDate); + var dateUpload = date.getTime().toString(); + + + chapters.push({ url: chapLink, name: chapTitle, dateUpload }) + + }) + + + return { name, link,imageUrl, description, genre, status, author, artist, chapters } } // For novel html content async getHtmlContent(url) { From 6b6ae6c8bc0df5f8524499434d8c8f8189403fb4 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Sun, 20 Apr 2025 15:57:51 +0530 Subject: [PATCH 3/4] manga(readcomiconline): Added page decoder & page list --- javascript/manga/src/en/readcomiconline.js | 142 ++++++++++++++++++--- 1 file changed, 126 insertions(+), 16 deletions(-) diff --git a/javascript/manga/src/en/readcomiconline.js b/javascript/manga/src/en/readcomiconline.js index fd941cc5..47c3a87c 100644 --- a/javascript/manga/src/en/readcomiconline.js +++ b/javascript/manga/src/en/readcomiconline.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/", "typeSource": "single", "itemType": 0, - "version": "0.0.2", + "version": "0.1.0", "pkgPath": "" }]; @@ -16,6 +16,10 @@ class DefaultExtension extends MProvider { this.client = new Client(); } + getPreference(key) { + return new SharedPreferences().get(key); + } + getHeaders() { return { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6832.64 Safari/537.36", @@ -142,23 +146,40 @@ class DefaultExtension extends MProvider { }) - return { name, link,imageUrl, description, genre, status, author, artist, 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"); + return { name, link, imageUrl, description, genre, status, author, artist, chapters } } + // For manga chapter pages async getPageList(url) { - throw new Error("getPageList not implemented"); + var pages = []; + var hdr = this.getHeaders() + let match; + var imageQuality = this.getPreference("readcomiconline_page_quality"); + + var doc = await this.request(url) + var html = doc.html + + // Find host url for images + var baseUrlOverride = "" + const hostRegex = /return\s+baeu\s*\(\s*l\s*,\s*'([^']+?)'\s*\);?/g; + match = hostRegex.exec(html) + if (match.length > 0) { + baseUrlOverride = match[1] + if (baseUrlOverride.slice(-1) != "/") baseUrlOverride += "/" + } + + + const pageRegex = /pht\s*=\s*'([^']+?)';?/g; + while ((match = pageRegex.exec(html)) !== null) { + var encodedImageUrl = match[1] + var decodedImageUrl = this.decodeImageUrl(encodedImageUrl, imageQuality, baseUrlOverride) + pages.push({ + url: decodedImageUrl, + headers: hdr, + }); + + } + return pages; } getFilterList() { function formateState(type_name, items, values) { @@ -222,6 +243,95 @@ class DefaultExtension extends MProvider { return filters; } getSourcePreferences() { - throw new Error("getSourcePreferences not implemented"); + return [ + { + key: "readcomiconline_page_quality", + listPreference: { + title: 'Preferred image quality', + summary: '', + valueIndex: 2, + entries: ["Low", "Medium", "High", "Highest"], + entryValues: ["l", "m", "h", "vh"] + } + }, + ] + } + + // -------- ReadComicOnline Image Decoder -------- + // Source:- https://readcomiconline.li/Scripts/rguard.min.js + + base64UrlDecode(input) { + let base64 = input + .replace(/-/g, "+") + .replace(/_/g, "/"); + + while (base64.length % 4 !== 0) { + base64 += "="; + } + + const base64abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + const outputBytes = []; + + for (let i = 0; i < base64.length; i += 4) { + const c1 = base64abc.indexOf(base64[i]); + const c2 = base64abc.indexOf(base64[i + 1]); + const c3 = base64abc.indexOf(base64[i + 2]); + const c4 = base64abc.indexOf(base64[i + 3]); + + const triplet = (c1 << 18) | (c2 << 12) | ((c3 & 63) << 6) | (c4 & 63); + + outputBytes.push((triplet >> 16) & 0xFF); + if (base64[i + 2] !== "=") outputBytes.push((triplet >> 8) & 0xFF); + if (base64[i + 3] !== "=") outputBytes.push(triplet & 0xFF); + } + + // Convert bytes to ISO-8859-1 string + return String.fromCharCode(...outputBytes); + } + + + extractBeforeDecode(url) { + return url.substring(15, 33) + url.substring(50); + } + + + finalizeDecodedString(decoded) { + return decoded.substring(0, decoded.length - 11) + decoded[decoded.length - 2] + decoded[decoded.length - 1]; + } + + decoderFunction(encodedUrl) { + var decodedUrl = this.extractBeforeDecode(encodedUrl); + decodedUrl = this.finalizeDecodedString(decodedUrl); + decodedUrl = decodeURIComponent(this.base64UrlDecode(decodedUrl)); + decodedUrl = decodedUrl.substring(0, 13) + decodedUrl.substring(17); + return decodedUrl.slice(0, -2) + "=s1600" + } + + decodeImageUrl(encodedImageUrl, imageQuality, baseUrlOverride) { + // Default image qualities + var IMAGEQUALITY = [ + { "l": "900", "m": "0", "h": "1600", "vh": "2041" }, + { "l": "900", "m": "1600", "h": "2041", "vh": "0" } + ] + + let finalUrl; + var qType = 0 + // Check if the url starts with https, if not then decode the url + if (!encodedImageUrl.startsWith("https")) { + encodedImageUrl = encodedImageUrl + .replace(/6UUQS__ACd__/g, 'b') + .replace(/pw_.g28x/g, "b") + + var encodedUrl = encodedImageUrl.split("=s")[0] + var decodedUrl = this.decoderFunction(encodedUrl); + + var queryParams = encodedImageUrl.substring(encodedImageUrl.indexOf("?")); + finalUrl = baseUrlOverride + decodedUrl + queryParams + } else { + // If the url starts with https, then just override the base url + qType = 1 + finalUrl = baseUrlOverride + encodedImageUrl.split(".com/")[1] + } + return finalUrl.replace("s1600", `s${IMAGEQUALITY[qType][imageQuality]}`) } } From 9b56b805c995aefdc6513d18d10dc2945706b878 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Sun, 20 Apr 2025 16:01:54 +0530 Subject: [PATCH 4/4] manga(readcomiconline): Remove "_" from begining of chapter title --- javascript/manga/src/en/readcomiconline.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/javascript/manga/src/en/readcomiconline.js b/javascript/manga/src/en/readcomiconline.js index 47c3a87c..da47bb40 100644 --- a/javascript/manga/src/en/readcomiconline.js +++ b/javascript/manga/src/en/readcomiconline.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://readcomiconline.li/", "typeSource": "single", "itemType": 0, - "version": "0.1.0", + "version": "0.1.1", "pkgPath": "" }]; @@ -135,7 +135,10 @@ class DefaultExtension extends MProvider { var tds = item.select("td") var aTag = tds[0].selectFirst("a") var chapLink = aTag.getHref + var chapTitle = aTag.text.trim().replace(`${name} `, "") + chapTitle = chapTitle[0] == "_" ? chapTitle.substring(1,) : chapTitle + var uploadDate = tds[1].text.trim() var date = new Date(uploadDate); var dateUpload = date.getTime().toString(); @@ -145,7 +148,6 @@ class DefaultExtension extends MProvider { }) - return { name, link, imageUrl, description, genre, status, author, artist, chapters } }