Merge pull request #246 from Swakshan/manga/readcomiconline

New source: ReadComicOnline
This commit is contained in:
Moustapha Kodjo Amadou
2025-04-20 12:01:33 +01:00
committed by GitHub

View File

@@ -0,0 +1,339 @@
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.1.1",
"pkgPath": ""
}];
class DefaultExtension extends MProvider {
constructor() {
super();
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",
"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 = imageSlug.includes("http") ? imageSlug : `${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) {
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} `, "")
chapTitle = chapTitle[0] == "_" ? chapTitle.substring(1,) : chapTitle
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 manga chapter pages
async getPageList(url) {
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) {
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() {
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]}`)
}
}