diff --git a/javascript/novel/src/ar/kolnovel.js b/javascript/novel/src/ar/kolnovel.js
new file mode 100644
index 00000000..1ed9ff2e
--- /dev/null
+++ b/javascript/novel/src/ar/kolnovel.js
@@ -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 `
${this.cleanTitle(html.title.rendered)}
${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("?") ? "&" : "?";
+ }
+}