Merge pull request #294 from xMohnad/novel/kolnovel

Novel/kolnovel
This commit is contained in:
Moustapha Kodjo Amadou
2025-06-21 20:04:37 +01:00
committed by GitHub

View File

@@ -0,0 +1,493 @@
// prettier-ignore
const mangayomiSources = [{
"name": "ملوك الروايات",
"lang": "ar",
"baseUrl": "https://kolnovel.com",
"apiUrl": "",
"iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://kolnovel.com",
"typeSource": "single",
"itemType": 2,
"version": "0.0.1",
"pkgPath": "novel/src/ar/kolnovel.js",
"notes": ""
}];
class DefaultExtension extends MProvider {
headers = {
"Sec-Fetch-Mode": "cors",
"Accept-Encoding": "gzip, deflate",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
};
getHeaders(url) {
throw new Error("getHeaders not implemented");
}
cleanTitle(title) {
if (!/[-_&@#%^)(*\s]*(كول|kol)/i.test(title)) return title;
title = title.replace(/[-_&@#%^)(*\s]*(كول|kol)/i, "");
title = title.replace(/[-_&@#%^)(*+،؛:]+/g, " ");
title = title.replace(/\s+/g, " ").trim();
return title;
}
novelFromElement(res) {
const doc = new Document(res.body);
const elements = doc.select("div.listupd article");
const list = [];
for (const el of elements) {
const name = this.cleanTitle(el.selectFirst("h2 a").text);
const imageUrl = el.selectFirst("img").getSrc;
const link = el.selectFirst("h2 a").getHref;
list.push({ name, imageUrl, link });
}
const hasNextPage = doc.selectFirst("div.hpage > a.r").text == "Next ";
return { list, hasNextPage };
}
novelFromJson(json) {
const list = [];
for (const el of json.series[0].all) {
const name = this.cleanTitle(el.post_title);
const imageUrl = el.post_image;
const link = el.post_link;
list.push({ name, imageUrl, link });
}
return { list, hasNextPage: false };
}
async getPopular(page) {
const res = await new Client().get(
`${this.getBaseUrl()}/series/?page=${page}&order=popular`,
this.headers,
);
return this.novelFromElement(res);
}
async getLatestUpdates(page) {
const res = await new Client().get(
`${this.getBaseUrl()}/series/?page=${page}&order=update`,
this.headers,
);
return this.novelFromElement(res);
}
async search(query, page, filters) {
const keyword = query.trim().replace(/\s+/g, "+");
if (keyword) {
const res = await new Client().post(
`${this.getBaseUrl()}/wp-admin/admin-ajax.php`,
{
"Content-Type": "application/x-www-form-urlencoded",
},
`action=ts_ac_do_search&ts_ac_query=${keyword}`,
);
return this.novelFromJson(JSON.parse(res.body));
}
let url = `${this.getBaseUrl()}/series/?page=${page}`;
filters.forEach((filter) => {
if (filter.type === "GenreFilter") {
const genre = filter.state.filter((e) => e.state);
genre.forEach((gen) => (url += `${this.ll(url)}genre[]=${gen.value}`));
} else if (filter.type === "TypeFilter") {
const type = filter.state.filter((e) => e.state);
type.forEach((ty) => (url += `${this.ll(url)}type[]=${ty.value}`));
} else if (filter.type === "OrderFilter") {
if (filter.values?.[filter.state]?.value)
url += `${this.ll(url)}order=${filter.values[filter.state].value}`;
} else if (filter.type === "StatusFilter") {
if (filter.values?.[filter.state]?.value)
url += `${this.ll(url)}status=${filter.values[filter.state].value}`;
}
});
const res = await new Client().get(url, this.headers);
return this.novelFromElement(res);
}
toStatus(status) {
return (
{
Ongoing: 0,
Completed: 1,
Hiatus: 2,
}[status] ?? 5 // 5 => unknown
);
}
async getDetail(url) {
const res = await new Client().get(url, this.headers);
const doc = new Document(res.body);
const info = doc.selectFirst("div.sertoinfo");
const subInfo = info.selectFirst("div.sertoauth");
const rewName = info.selectFirst("h1.entry-title").text;
const name = this.cleanTitle(rewName);
const imageUrl = doc.selectFirst("img.attachment-post-thumbnail")?.getSrc;
const scanlator = subInfo.selectFirst(".serl:contains('المترجم') a").text;
let description =
info.selectFirst("div.sersys.entry-content p").text + "\n\n";
const lang = subInfo.selectFirst(
".serl:contains('اللغة الأم') .serval",
)?.text;
if (lang) description += `اللغة الأم: ${lang}\n`;
const releaseYear = subInfo.selectFirst(
".serl:contains('صدر في سنة') .serval",
)?.text;
if (releaseYear) description += `سنة الصدور: ${releaseYear}\n`;
const types = subInfo
.select(".serl:contains('نوع') a")
.map((el) => el.text)
.join(", ");
if (types) description += `الانواع: ${types}\n`;
const altTitle = this.cleanTitle(info.selectFirst("span.alter")?.text);
if (altTitle) description += `اسم آخر للعمل: ${altTitle}\n`;
const genre = info.select("div.sertogenre a").map((el) => el.text);
const author = subInfo.selectFirst(".serl:contains('الكاتب') a").text;
const status = this.toStatus(info.selectFirst("div.sertostat span").text);
const chapters = [];
for (const el of doc.select("div.sertobody div.bixbox ul li > a")) {
const url = el.getHref;
const dateUpload = this.parseDate(el.selectFirst("div.epl-date").text);
// Chapter name
let title = el.selectFirst("div.epl-title").text.trim();
const num = el.selectFirst("div.epl-num").text.trim();
if (title.includes(num)) title = title.replace(num, "").trim();
if (title.includes(rewName)) title = title.replace(rewName, "").trim();
if (title.includes(name)) title = title.replace(name, "").trim();
const numMatch = num.match(/(?:الفصل|chapter)\s+(\d+(?:\.\d+)?)/i);
if (numMatch) {
title = title.replace(numMatch[0], "").trim();
if (!title.includes(`(${numMatch[1]})`)) {
title = title.replace(numMatch[1], "").trim();
}
}
title = title.replace(/\s{2,}/g, " ").trim();
const finalName =
title && num ? `${num}: ${title}` : title ? title : num ? num : "?";
chapters.push({ name: finalName, url, dateUpload, scanlator });
}
return {
name,
imageUrl,
description,
genre,
author,
status,
chapters,
};
}
extractIdFromUrl(url) {
const match = url.match(/-(\d+)\/?$/);
return match ? match[1] : null;
}
// For novel html content
async getHtmlContent(name, url) {
const id = this.extractIdFromUrl(url);
const res = await new Client().get(
`${this.getBaseUrl()}/wp-json/wp/v2/posts/${id}`,
this.headers,
);
return this.cleanHtmlContent(JSON.parse(res.body));
}
// Clean html up for reader
async cleanHtmlContent(html) {
return `<h2 style="text-align: center;">${this.cleanTitle(html.title.rendered)}</h2><hr><br>${html.content.rendered}`;
}
getFilterList() {
return [
{
type: "StatusFilter",
name: "الحالة",
type_name: "SelectFilter",
values: [
{
type_name: "SelectOption",
name: "الكل",
value: "",
},
{
type_name: "SelectOption",
name: "مستمر",
value: "ongoing",
},
{
type_name: "SelectOption",
name: "متوقف مؤقتًا",
value: "hiatus",
},
{
type_name: "SelectOption",
name: "مكتمل",
value: "completed",
},
],
state: 0,
},
{
type: "OrderFilter",
name: "ترتيب حسب",
type_name: "SelectFilter",
values: [
{
type_name: "SelectOption",
name: "الإعداد الأولي",
value: "",
},
{
type_name: "SelectOption",
name: "A-Z",
value: "title",
},
{
type_name: "SelectOption",
name: "Z-A",
value: "titlereverse",
},
{
type_name: "SelectOption",
name: "أخر التحديثات",
value: "update",
},
{
type_name: "SelectOption",
name: "أخر ما تم إضافته",
value: "latest",
},
{
type_name: "SelectOption",
name: "الرائجة",
value: "popular",
},
{
type_name: "SelectOption",
name: "التقييم",
value: "rating",
},
],
state: 0,
},
{
type_name: "GroupFilter",
type: "GenreFilter",
name: "تصنيف",
state: [
["Romance", "romance"],
["Shounen Ai", "shounen-ai"],
["Wuxia", "wuxia"],
["Xianxia", "xianxia"],
["XUANHUAN", "xuanhuan"],
[
"أبطال خارقين",
"%d8%a3%d8%a8%d8%b7%d8%a7%d9%84-%d8%ae%d8%a7%d8%b1%d9%82%d9%8a%d9%86",
],
["أساطير", "%d8%a3%d8%b3%d8%a7%d8%b7%d9%8a%d8%b1"],
["أشباح", "%d8%a3%d8%b4%d8%a8%d8%a7%d8%ad"],
["أكشن", "action"],
["ألعاب", "%d8%a3%d9%84%d8%b9%d8%a7%d8%a8"],
["إثارة", "excitement"],
["إسلامي", "%d8%a5%d8%b3%d9%84%d8%a7%d9%85%d9%8a"],
["إنتقال الى عالم أخر", "isekai"],
["إيتشي", "etchi"],
["اكاديمي", "%d8%a7%d9%83%d8%a7%d8%af%d9%8a%d9%85%d9%8a"],
["اكشن", "%d8%a7%d9%83%d8%b4%d9%86"],
["الإثارة", "%d8%a7%d9%84%d8%a5%d8%ab%d8%a7%d8%b1%d8%a9"],
["الخيال العلمي", "sci-fi"],
["الدراما", "%d8%a7%d9%84%d8%af%d8%b1%d8%a7%d9%85%d8%a7"],
[
"المغامرات",
"%d8%a7%d9%84%d9%85%d8%ba%d8%a7%d9%85%d8%b1%d8%a7%d8%aa",
],
["انتقام", "%d8%a7%d9%86%d8%aa%d9%82%d8%a7%d9%85"],
["بطل مضاد", "%d8%a8%d8%b7%d9%84-%d9%85%d8%b6%d8%a7%d8%af"],
["بطل ناضج", "%d8%a8%d8%b7%d9%84-%d9%86%d8%a7%d8%b6%d8%ac"],
["بقاء", "%d8%a8%d9%82%d8%a7%d8%a1"],
[
"بناء مملكة",
"%d8%a8%d9%86%d8%a7%d8%a1-%d9%85%d9%85%d9%84%d9%83%d8%a9",
],
["بوليسي", "policy"],
["تاريخ", "%d8%aa%d8%a7%d8%b1%d9%8a%d8%ae"],
["تاريخي", "historical"],
["تحقيقات", "%d8%aa%d8%ad%d9%82%d9%8a%d9%82"],
["تشويق", "%d8%aa%d8%b4%d9%88%d9%8a%d9%82"],
["تقمص شخصيات", "rpg"],
["تلاعب", "%d8%aa%d9%84%d8%a7%d8%b9%d8%a8"],
["تناسخ", "%d8%aa%d9%86%d8%a7%d8%b3%d8%ae"],
["جريمة", "crime"],
["جوسى", "josei"],
["جوسي", "%d8%ac%d9%88%d8%b3%d9%8a"],
["حريم", "harem"],
[
"حل الألغاز",
"%d8%ad%d9%84-%d8%a7%d9%84%d8%a3%d9%84%d8%ba%d8%a7%d8%b2",
],
["حياة مدرسية", "school-life"],
[
"خارق للطبيعة",
"%d8%ae%d8%a7%d8%b1%d9%82-%d9%84%d9%84%d8%b7%d8%a8%d9%8a%d8%b9%d8%a9",
],
["خيال", "%d8%ae%d9%8a%d8%a7%d9%84"],
["خيال علمي", "%d8%ae%d9%8a%d8%a7%d9%84-%d8%b9%d9%84%d9%85%d9%8a"],
["خيالي", "%d8%ae%d9%8a%d8%a7%d9%84%d9%8a"],
["خيالي(فانتازيا)", "fantasy"],
["دراما", "drama"],
["درامي", "%d8%af%d8%b1%d8%a7%d9%85%d9%8a"],
["رعب", "horror"],
["رعب كوني", "%d8%b1%d8%b9%d8%a8-%d9%83%d9%88%d9%86%d9%8a"],
["رعب نفسي", "%d8%b1%d8%b9%d8%a8-%d9%86%d9%81%d8%b3%d9%8a"],
["رومانسي", "romantic"],
["رومانسية", "%d8%b1%d9%88%d9%85%d8%a7%d9%86%d8%b3%d9%8a%d8%a9"],
["رومنسية", "%d8%b1%d9%88%d9%85%d9%86%d8%b3%d9%8a%d8%a9"],
["زنزانة", "%d8%b2%d9%86%d8%b2%d8%a7%d9%86%d8%a9"],
["زيانشيا", "%d8%b2%d9%8a%d8%a7%d9%86%d8%b4%d9%8a%d8%a7"],
["ستيم بانك", "%d8%b3%d8%aa%d9%8a%d9%85-%d8%a8%d8%a7%d9%86%d9%83"],
["سحر", "magic"],
[
"سفر بالزمن",
"%d8%b3%d9%81%d8%b1-%d8%a8%d8%a7%d9%84%d8%b2%d9%85%d9%86",
],
[
"سفر عبر الزمن",
"%d8%b3%d9%81%d8%b1-%d8%b9%d8%a8%d8%b1-%d8%a7%d9%84%d8%b2%d9%85%d9%86",
],
["سياسة", "%d8%b3%d9%8a%d8%a7%d8%b3%d8%a9"],
["سينن", "senen"],
["شريحة من الحياة", "slice-of-life"],
["شعر", "%d8%b4%d8%b9%d8%b1"],
["شوانهوان", "%d8%b4%d9%88%d8%a7%d9%86%d9%87%d9%88%d8%a7%d9%86"],
["شوجو", "shojo"],
["شونين", "shonen"],
["طبي", "medical"],
["ظواهر خارقة للطبيعة", "supernatural"],
["عائلي", "%d8%b9%d8%a7%d8%a6%d9%84%d9%8a"],
["عموض", "%d8%b9%d9%85%d9%88%d8%b6"],
["غموض", "mysteries"],
["فانتازي", "%d9%81%d8%a7%d9%86%d8%aa%d8%a7%d8%b2%d9%8a"],
["فانتازيا", "%d9%81%d8%a7%d9%86%d8%aa%d8%a7%d8%b2%d9%8a%d8%a7"],
["فانفيك", "%d9%81%d8%a7%d9%86%d9%81%d9%8a%d9%83"],
["فنتازيا", "%d9%81%d9%86%d8%aa%d8%a7%d8%b2%d9%8a%d8%a7"],
["فنون القتال", "martial-arts"],
["فنون قتال", "%d9%81%d9%86%d9%88%d9%86-%d9%82%d8%aa%d8%a7%d9%84"],
["قصة قصيرة", "%d9%82%d8%b5%d8%a9-%d9%82%d8%b5%d9%8a%d8%b1%d8%a9"],
["قوة خارقة", "%d9%82%d9%88%d8%a9-%d8%ae%d8%a7%d8%b1%d9%82%d8%a9"],
["قوى خارقة", "superpower"],
["كوميدي", "comedy"],
["كوميديا", "%d9%83%d9%88%d9%85%d9%8a%d8%af%d9%8a%d8%a7"],
["كوميدية", "%d9%83%d9%88%d9%85%d9%8a%d8%af%d9%8a%d8%a9"],
["مأساة", "%d9%85%d8%a3%d8%b3%d8%a7%d8%a9"],
["مأساوي", "tragedy"],
["مؤامرة", "%d9%85%d8%a4%d8%a7%d9%85%d8%b1%d8%a9"],
["ما بعد الكارثة", "after-the-disaster"],
[
"ما بعد نهاية العالم",
"%d9%85%d8%a7-%d8%a8%d8%b9%d8%af-%d9%86%d9%87%d8%a7%d9%8a%d8%a9-%d8%a7%d9%84%d8%b9%d8%a7%d9%84%d9%85",
],
[
"مضاد البطل",
"%d9%85%d8%b6%d8%a7%d8%af-%d8%a7%d9%84%d8%a8%d8%b7%d9%84",
],
["مغامرات", "%d9%85%d8%ba%d8%a7%d9%85%d8%b1%d8%a7%d8%aa"],
["مغامرة", "adventure"],
["ميكا", "mechanical"],
["ناضج", "mature"],
["نظام", "%d9%86%d8%b8%d8%a7%d9%85"],
["نفسي", "psychological"],
["ون شوت", "%d9%88%d9%86-%d8%b4%d9%88%d8%aa"],
["ووكسيا", "%d9%88%d9%88%d9%83%d8%b3%d9%8a%d8%a7"],
].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
},
{
type_name: "GroupFilter",
type: "TypeFilter",
name: "النوع",
state: [
["إنجليزية", "english"],
["رواية لايت", "light-novel"],
[
"رواية مؤلفة",
"%d8%b1%d9%88%d8%a7%d9%8a%d8%a9-%d9%85%d8%a4%d9%84%d9%81%d8%a9",
],
["رواية ويب", "web-novel"],
["صينية", "chinese"],
["عربية", "arabic"],
["كورية", "korean"],
["مؤلفة", "%d9%85%d8%a4%d9%84%d9%81%d8%a9"],
["ون شوت", "%d9%88%d9%86-%d8%b4%d9%88%d8%aa"],
["يابانية", "japanese"],
].map((x) => ({ type_name: "CheckBox", name: x[0], value: x[1] })),
},
];
}
getBaseUrl() {
const preference = new SharedPreferences();
var base_url = preference.get("base_url");
if (base_url.length == 0) {
return this.source.baseUrl;
}
if (base_url.endsWith("/")) {
return base_url.slice(0, -1);
}
return base_url;
}
getSourcePreferences() {
return [
{
key: "base_url",
editTextPreference: {
title: "تعديل الرابط",
summary: "",
value: this.source.baseUrl,
dialogTitle: "تعديل",
dialogMessage: `Defaul URL ${this.source.baseUrl}`,
},
},
];
}
parseDate(date) {
const months = {
يناير: "January",
فبراير: "February",
مارس: "March",
أبريل: "April",
ابريل: "April",
مايو: "May",
يونيو: "June",
يوليو: "July",
أغسطس: "August",
اغسطس: "August",
سبتمبر: "September",
أكتوبر: "October",
اكتوبر: "October",
نوفمبر: "November",
ديسمبر: "December",
};
const [monthAr, day, year] = date.split(/[\s,]+/);
const monthEnglish = months[monthAr] || "";
if (!monthEnglish) return "";
return new Date(`${monthEnglish} ${day}, ${year}`).getTime().toString();
}
ll(url) {
return url.includes("?") ? "&" : "?";
}
}