From 14691c3ddabf52522d3f9cdfc09d2a776cec093d Mon Sep 17 00:00:00 2001 From: Swakshan Date: Tue, 15 Apr 2025 21:58:43 +0530 Subject: [PATCH 01/10] anime(animekai): Added popular & latest --- javascript/anime/src/en/animekai.js | 157 ++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 javascript/anime/src/en/animekai.js diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js new file mode 100644 index 00000000..8b7dae68 --- /dev/null +++ b/javascript/anime/src/en/animekai.js @@ -0,0 +1,157 @@ +const mangayomiSources = [{ + "name": "AnimeKai", + "lang": "en", + "baseUrl": "https://animekai.to", + "apiUrl": "", + "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", + "typeSource": "single", + "itemType": 1, + "version": "0.0.1", + "pkgPath": "anime/src/en/animekai.js" +}]; + +class DefaultExtension extends MProvider { + getHeaders(url) { + throw new Error("getHeaders not implemented"); + } + + constructor() { + super(); + this.client = new Client(); + } + + getPreference(key) { + return new SharedPreferences().get(key); + } + + getBaseUrl() { + return this.getPreference("animekai_base_url"); + + } + + async request(slug) { + var url = this.getBaseUrl() + slug; + var res = await this.client.get(url); + return res.body + } + + async getPage(slug) { + var res = await this.request(slug); + return new Document(res); + } + + async searchPage({ query = "", type = [], genre = [], status = [], sort = "", season = [], year = [], rating = [], country = [], subType = [], page = 1 } = {}) { + + function bundleSlug(category, items) { + var rd = "" + for (var item of items) { + rd += `&${category}[]=${item}`; + } + return rd; + } + + var slug = "/browser?" + + slug += "keyword=" + query; + + slug += bundleSlug("type", type); + slug += bundleSlug("genre", genre); + slug += bundleSlug("status", status); + slug += "&sort=" + sort; + + slug += bundleSlug("status", status); + slug += bundleSlug("season", season); + slug += bundleSlug("year", year); + slug += bundleSlug("rating", rating); + slug += bundleSlug("country", country); + slug += bundleSlug("subType", subType); + slug += `&page=${page}`; + + var body = await this.getPage(slug); + + var paginations = body.select(".pagination > li") + var hasNextPage = !paginations[paginations.length - 1].className.includes("active") + var list = [] + + var titlePref = this.getPreference("animekai_title_lang") + var animes = body.selectFirst(".aitem-wrapper").select(".aitem") + animes.forEach(anime => { + var link = anime.selectFirst("a").getHref + var imageUrl = anime.selectFirst("img").attr("data-src") + var name = anime.selectFirst("a.title").attr(titlePref) + list.push({ name, link, imageUrl }); + }) + + return { list, hasNextPage } + } + + + async getPopular(page) { + var types = this.getPreference("animekai_popular_latest_type") + return await this.searchPage({ sort: "trending", type: types, page: page }); + } + get supportsLatest() { + throw new Error("supportsLatest not implemented"); + } + async getLatestUpdates(page) { + var types = this.getPreference("animekai_popular_latest_type") + return await this.searchPage({ sort: "updated_date", type: types, page: page }); + } + async search(query, page, filters) { + throw new Error("search not implemented"); + } + 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() { + throw new Error("getFilterList not implemented"); + } + getSourcePreferences() { + return [ + { + key: "animekai_base_url", + editTextPreference: { + title: "Override base url", + summary: "", + value: "https://animekai.to", + dialogTitle: "Override base url", + dialogMessage: "", + } + }, { + key: "animekai_popular_latest_type", + multiSelectListPreference: { + title: 'Preferred type of anime to be shown in popular & latest section', + summary: 'Choose which type of anime you want to see in the popular &latest section', + values: ["tv", "special", "ova", "ona"], + entries: ["TV", "Special", "OVA", "ONA", "Music", "Movie"], + entryValues: ["tv", "special", "ova", "ona", "music", "movie"] + } + }, { + key: "animekai_title_lang", + listPreference: { + title: 'Preferred title language', + summary: 'Choose in which language anime title should be shown', + valueIndex: 1, + entries: ["English", "Romaji"], + entryValues: ["title", "data-jp"] + } + } + ] + } +} From 1f7f518913821b9e2416d1746a949bc3eb4152c4 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Tue, 15 Apr 2025 23:37:54 +0530 Subject: [PATCH 02/10] anime(animekai): Added search & filters --- javascript/anime/src/en/animekai.js | 155 ++++++++++++++++++++++++++-- 1 file changed, 149 insertions(+), 6 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 8b7dae68..8f2ad9cd 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.0.1", + "version": "0.0.2", "pkgPath": "anime/src/en/animekai.js" }]; @@ -40,7 +40,7 @@ class DefaultExtension extends MProvider { return new Document(res); } - async searchPage({ query = "", type = [], genre = [], status = [], sort = "", season = [], year = [], rating = [], country = [], subType = [], page = 1 } = {}) { + async searchPage({ query = "", type = [], genre = [], status = [], sort = "", season = [], year = [], rating = [], country = [], language = [], page = 1 } = {}) { function bundleSlug(category, items) { var rd = "" @@ -64,14 +64,17 @@ class DefaultExtension extends MProvider { slug += bundleSlug("year", year); slug += bundleSlug("rating", rating); slug += bundleSlug("country", country); - slug += bundleSlug("subType", subType); + slug += bundleSlug("language", language); slug += `&page=${page}`; + var list = [] + var hasNextPage = false + var body = await this.getPage(slug); var paginations = body.select(".pagination > li") + var hasNextPage = !paginations[paginations.length - 1].className.includes("active") - var list = [] var titlePref = this.getPreference("animekai_title_lang") var animes = body.selectFirst(".aitem-wrapper").select(".aitem") @@ -98,7 +101,27 @@ class DefaultExtension extends MProvider { return await this.searchPage({ sort: "updated_date", type: types, page: page }); } async search(query, page, filters) { - throw new Error("search not implemented"); + function getFilter(state) { + var rd = [] + state.forEach(item => { + if (item.state) { + rd.push(item.value) + + } + }) + return rd + + } + var type = getFilter(filters[0].state) + var genre = getFilter(filters[1].state) + var status = getFilter(filters[2].state) + var sort = filters[3].values[filters[3].state].value + var season = getFilter(filters[4].state) + var year = getFilter(filters[5].state) + var rating = getFilter(filters[6].state) + var country = getFilter(filters[7].state) + var language = getFilter(filters[8].state) + return await this.searchPage({ query, type, genre, status, sort, season, year, rating, country, language, page }); } async getDetail(url) { throw new Error("getDetail not implemented"); @@ -120,7 +143,127 @@ class DefaultExtension extends MProvider { throw new Error("getPageList not implemented"); } getFilterList() { - throw new Error("getFilterList not implemented"); + 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 = []; + + // Types + var items = ["TV", "Special", "OVA", "ONA", "Music", "Movie"] + var values = ["tv", "special", "ova", "ona", "music", "movie"] + filters.push({ + type_name: "GroupFilter", + name: "Types", + state: formateState("CheckBox", items, values) + }) + + // Genre + items = [ + "Action", "Adventure", "Avant Garde", "Boys Love", "Comedy", "Demons", "Drama", "Ecchi", "Fantasy", + "Girls Love", "Gourmet", "Harem", "Horror", "Isekai", "Iyashikei", "Josei", "Kids", "Magic", + "Mahou Shoujo", "Martial Arts", "Mecha", "Military", "Music", "Mystery", "Parody", "Psychological", + "Reverse Harem", "Romance", "School", "Sci-Fi", "Seinen", "Shoujo", "Shounen", "Slice of Life", + "Space", "Sports", "Super Power", "Supernatural", "Suspense", "Thriller", "Vampire" + ]; + + values = [ + "47", "1", "235", "184", "7", "127", "66", "8", "34", "926", "436", "196", "421", "77", "225", + "555", "35", "78", "857", "92", "219", "134", "27", "48", "356", "240", "798", "145", "9", "36", + "189", "183", "37", "125", "220", "10", "350", "49", "322", "241", "126" + ]; + + filters.push({ + type_name: "GroupFilter", + name: "Genres", + state: formateState("CheckBox", items, values) + }) + + // Status + items = ["Not Yet Aired", "Releasing", "Completed"] + values = ["info", "releasing", "completed"] + filters.push({ + type_name: "GroupFilter", + name: "Status", + state: formateState("CheckBox", items, values) + }) + + // Sort + items = [ + "All", "Updated date", "Released date", "End date", "Added date", "Trending", + "Name A-Z", "Average score", "MAL score", "Total views", "Total bookmarks", "Total episodes" + ]; + + values = [ + "", "updated_date", "released_date", "end_date", "added_date", "trending", + "title_az", "avg_score", "mal_score", "total_views", "total_bookmarks", "total_episodes" + ]; + filters.push({ + type_name: "SelectFilter", + name: "Sort by", + state: 0, + values: formateState("SelectOption", items, values) + }) + + // Season + items = ["Fall", "Summer", "Spring", "Winter", "Unknown"]; + values = ["fall", "summer", "spring", "winter", "unknown"]; + filters.push({ + type_name: "GroupFilter", + name: "Season", + state: formateState("CheckBox", items, values) + }) + + // Years + const currentYear = new Date().getFullYear(); + var years = Array.from({ length: currentYear - 1999 }, (_, i) => (2000 + i).toString()).reverse() + items = [...years, "1990s", "1980s", "1970s", "1960s", "1950s", "1940s", "1930s", "1920s", "1910s", "1900s",] + filters.push({ + type_name: "GroupFilter", + name: "Years", + state: formateState("CheckBox", items, items) + }) + + // Ratings + items = [ + "G - All Ages", + "PG - Children", + "PG 13 - Teens 13 and Older", + "R - 17+, Violence & Profanity", + "R+ - Profanity & Mild Nudity", + "Rx - Hentai" + ]; + + values = ["g", "pg", "pg_13", "r", "r+", "rx"]; + filters.push({ + type_name: "GroupFilter", + name: "Ratings", + state: formateState("CheckBox", items, items) + }) + + // Country + items = ["Japan", "China"]; + values = ["11", "2"]; + filters.push({ + type_name: "GroupFilter", + name: "Country", + state: formateState("CheckBox", items, items) + }) + + // Language + items = ["Hard Sub", "Soft Sub", "Dub", "Sub & Dub"]; + values = ["sub", "softsub", "dub", "subdub"]; + filters.push({ + type_name: "GroupFilter", + name: "Language", + state: formateState("CheckBox", items, items) + }) + + return filters; } getSourcePreferences() { return [ From 2defdcd6d4e6ce674cf175596e58a749b6034522 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Tue, 15 Apr 2025 23:40:33 +0530 Subject: [PATCH 03/10] anime(animekai): pagination fix --- javascript/anime/src/en/animekai.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 8f2ad9cd..6cb92a08 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.0.2", + "version": "0.0.3", "pkgPath": "anime/src/en/animekai.js" }]; @@ -68,13 +68,11 @@ class DefaultExtension extends MProvider { slug += `&page=${page}`; var list = [] - var hasNextPage = false var body = await this.getPage(slug); var paginations = body.select(".pagination > li") - - var hasNextPage = !paginations[paginations.length - 1].className.includes("active") + var hasNextPage = paginations.length > 0 ? !paginations[paginations.length - 1].className.includes("active") : false var titlePref = this.getPreference("animekai_title_lang") var animes = body.selectFirst(".aitem-wrapper").select(".aitem") @@ -106,7 +104,6 @@ class DefaultExtension extends MProvider { state.forEach(item => { if (item.state) { rd.push(item.value) - } }) return rd From 28deec45923fe761e484eec6b9224f370f6233f0 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Wed, 16 Apr 2025 16:19:18 +0530 Subject: [PATCH 04/10] anime(animekai): Added kai decoders and anime details --- javascript/anime/src/en/animekai.js | 251 +++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 7 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 6cb92a08..6f15f424 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,20 +6,21 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.0.3", + "version": "0.1.0", "pkgPath": "anime/src/en/animekai.js" }]; class DefaultExtension extends MProvider { - getHeaders(url) { - throw new Error("getHeaders not implemented"); - } - constructor() { super(); this.client = new Client(); } + getHeaders(url) { + throw new Error("getHeaders not implemented"); + } + + getPreference(key) { return new SharedPreferences().get(key); } @@ -120,8 +121,108 @@ class DefaultExtension extends MProvider { var language = getFilter(filters[8].state) return await this.searchPage({ query, type, genre, status, sort, season, year, rating, country, language, page }); } + async getDetail(url) { - throw new Error("getDetail not implemented"); + function statusCode(status) { + return { + "Releasing": 0, + "Completed": 1, + "Not Yet Aired": 4, + }[status] ?? 5; + } + + var slug = url + var link = this.getBaseUrl() + slug + var body = await this.getPage(slug) + + var mainSection = body.selectFirst(".watch-section") + + var imageUrl = mainSection.selectFirst("div.poster").selectFirst("img").getSrc + + var namePref = this.getPreference("animekai_title_lang") + var nameSection = mainSection.selectFirst("div.title") + var name = namePref.includes("jp") ? nameSection.attr(namePref) : nameSection.text + + var description = mainSection.selectFirst("div.desc").text + + var detailSection = mainSection.select("div.detail > div") + + var genre = [] + var status = 5 + detailSection.forEach(item => { + var itemText = item.text.trim() + + if (itemText.includes("Genres")) { + genre = itemText.replace("Genres: ", "").split(", ") + } + if (itemText.includes("Status")) { + var statusText = item.selectFirst("span").text + status = statusCode(statusText) + } + }) + + var chapters = [] + var animeId = body.selectFirst("#anime-rating").attr("data-id") + + var token = await this.generateToken(animeId) + var res = await this.request(`/ajax/episodes/list?ani_id=${animeId}&_=${token}`) + body = JSON.parse(res) + if (body.status == 200) { + var doc = new Document(body["result"]) + var episodes = doc.selectFirst("div.eplist.titles").select("li") + var showUncenEp = this.getPreference("animekai_show_uncen_epsiodes") + + for (var item of episodes) { + var aTag = item.selectFirst("a") + + var num = parseInt(aTag.attr("num")) + var title = aTag.selectFirst("span").text + title = title.includes("Episode") ? "" : `: ${title}` + var epName = `Episode ${num}${title}` + + + var langs = aTag.attr("langs") + var scanlator = langs === "1" ? "SUB" : "SUB, DUB" + + var token = aTag.attr("token") + + var epData = { + name: epName, + url: token, + scanlator + } + + // Check if the episode is uncensored + var slug = aTag.attr("slug") + if (slug.includes("uncen")) { + + // if dont show uncensored episodes, skip this episode + if (!showUncenEp) continue + + scanlator += ", UNCENSORED" + epName = `Episode ${num}: (Uncensored)` + // Build for uncensored episode + epData = { + name: epName, + url: token, + scanlator + } + + // Check if the episode already exists as censored if so, add to existing data + var exData = chapters[num - 1] + if (exData) { + exData.url += "||" + epData.url + exData.scanlator += ", " + epData.scanlator + chapters[num - 1] = exData + continue + + } + } + chapters.push(epData) + } + } + chapters.reverse() + return { name, imageUrl, link, description, genre, status, chapters } } // For novel html content async getHtmlContent(url) { @@ -291,7 +392,143 @@ class DefaultExtension extends MProvider { entries: ["English", "Romaji"], entryValues: ["title", "data-jp"] } - } + }, + { + key: "animekai_show_uncen_epsiodes", + switchPreferenceCompat: { + title: 'Show uncensored episodes', + summary: "", + value: true + } + }, ] } + + //----------------AnimeKai Decoders---------------- + // Credits :- https://github.com/amarullz/kaicodex/ + + base64Decoder(base64) { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + let binary = ''; + + base64 = base64.replace(/=+$/, ''); + + for (let i = 0; i < base64.length; i++) { + const index = chars.indexOf(base64[i]); + if (index === -1) continue; // skip invalid characters + binary += index.toString(2).padStart(6, '0'); + } + + let decoded = ''; + for (let i = 0; i < binary.length; i += 8) { + const byte = binary.substring(i, i + 8); + if (byte.length < 8) continue; + decoded += String.fromCharCode(parseInt(byte, 2)); + } + + return decoded; + } + base64Encoder(str) { + const base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var out, i, len; + var c1, c2, c3; + len = str.length; + i = 0; + out = ""; + while (i < len) { + c1 = str.charCodeAt(i++) & 0xff; + if (i == len) { + out += base64EncodeChars.charAt(c1 >> 2); + out += base64EncodeChars.charAt((c1 & 0x3) << 4); + out += "=="; + break; + } + c2 = str.charCodeAt(i++); + if (i == len) { + out += base64EncodeChars.charAt(c1 >> 2); + out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); + out += base64EncodeChars.charAt((c2 & 0xF) << 2); + out += "="; + break; + } + c3 = str.charCodeAt(i++); + out += base64EncodeChars.charAt(c1 >> 2); + out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); + out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); + out += base64EncodeChars.charAt(c3 & 0x3F); + } + return out; + } + + transform(key, text) { + const v = Array.from({ length: 256 }, (_, i) => i); + let c = 0; + const f = []; + + for (let w = 0; w < 256; w++) { + c = (c + v[w] + key.charCodeAt(w % key.length)) % 256; + [v[w], v[c]] = [v[c], v[w]]; + } + + let a = 0, w = 0, sum = 0; + while (a < text.length) { + w = (w + 1) % 256; + sum = (sum + v[w]) % 256; + [v[w], v[sum]] = [v[sum], v[w]]; + f.push(String.fromCharCode(text.charCodeAt(a) ^ v[(v[w] + v[sum]) % 256])); + a++; + } + return f.join(''); + } + + reverseString(input) { + return input.split('').reverse().join(''); + } + + substitute(input, keys, values) { + const map = {}; + for (let i = 0; i < keys.length; i++) { + map[keys[i]] = values[i] || keys[i]; + } + return input.split('').map(char => map[char] || char).join(''); + } + + async getDecoderPattern() { + const preferences = new SharedPreferences(); + let pattern = preferences.getString("anime_kai_decoder_pattern", ""); + var pattern_ts = parseInt(preferences.getString("anime_kai_decoder_pattern_ts", "0")); + var now_ts = parseInt(new Date().getTime() / 1000); + + // pattern is checked from API every 30 minutes + if (now_ts - pattern_ts > 30 * 60) { + var res = await this.client.get("https://raw.githubusercontent.com/amarullz/kaicodex/refs/heads/main/generated/kai_codex.json") + pattern = res.body + preferences.setString("anime_kai_decoder_pattern", pattern); + preferences.setString("anime_kai_decoder_pattern_ts", `${now_ts}`); + } + + return JSON.parse(pattern); + } + + async patternExecutor(key, type, id) { + var result = id + var pattern = await this.getDecoderPattern() + var logic = pattern[key][type] + logic.forEach(step => { + var method = step[0] + if (method == "urlencode") result = encodeURIComponent(result); + else if (method == "rc4") result = this.transform(step[1], result); + else if (method == "reverse") result = this.reverseString(result); + else if (method == "substitute") result = this.substitute(result, step[1], step[2]); + else if (method == "safeb64_decode") result = this.base64Decoder(result); + else if (method == "safeb64_encode") result = this.base64Encoder(result); + }) + return result + } + + async generateToken(id) { + var token = await this.patternExecutor("kai", "encrypt", id) + return token; + } + } From c6e1e436ee8a4b097b65974dab6f67863803a2e7 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Wed, 16 Apr 2025 16:24:03 +0530 Subject: [PATCH 05/10] anime(animekai): set default sort --- javascript/anime/src/en/animekai.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 6f15f424..99496f66 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.1.0", + "version": "0.1.1", "pkgPath": "anime/src/en/animekai.js" }]; @@ -58,14 +58,14 @@ class DefaultExtension extends MProvider { slug += bundleSlug("type", type); slug += bundleSlug("genre", genre); slug += bundleSlug("status", status); - slug += "&sort=" + sort; - slug += bundleSlug("status", status); slug += bundleSlug("season", season); slug += bundleSlug("year", year); slug += bundleSlug("rating", rating); slug += bundleSlug("country", country); slug += bundleSlug("language", language); + sort = sort.length < 1 ? "most_relevance" : "" // default sort is most relevance + slug += "&sort=" + sort; slug += `&page=${page}`; var list = [] @@ -498,7 +498,7 @@ class DefaultExtension extends MProvider { let pattern = preferences.getString("anime_kai_decoder_pattern", ""); var pattern_ts = parseInt(preferences.getString("anime_kai_decoder_pattern_ts", "0")); var now_ts = parseInt(new Date().getTime() / 1000); - + // pattern is checked from API every 30 minutes if (now_ts - pattern_ts > 30 * 60) { var res = await this.client.get("https://raw.githubusercontent.com/amarullz/kaicodex/refs/heads/main/generated/kai_codex.json") @@ -509,7 +509,7 @@ class DefaultExtension extends MProvider { return JSON.parse(pattern); } - + async patternExecutor(key, type, id) { var result = id var pattern = await this.getDecoderPattern() From c2d3729ae51b04dd316dfb28b324458856e397f8 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Thu, 17 Apr 2025 22:27:00 +0530 Subject: [PATCH 06/10] anime(animekai): Added stream extractors --- javascript/anime/src/en/animekai.js | 257 ++++++++++++++++++++-------- 1 file changed, 184 insertions(+), 73 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 99496f66..4756588b 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.1.1", + "version": "0.2.0", "pkgPath": "anime/src/en/animekai.js" }]; @@ -16,11 +16,6 @@ class DefaultExtension extends MProvider { this.client = new Client(); } - getHeaders(url) { - throw new Error("getHeaders not implemented"); - } - - getPreference(key) { return new SharedPreferences().get(key); } @@ -54,7 +49,6 @@ class DefaultExtension extends MProvider { var slug = "/browser?" slug += "keyword=" + query; - slug += bundleSlug("type", type); slug += bundleSlug("genre", genre); slug += bundleSlug("status", status); @@ -87,18 +81,16 @@ class DefaultExtension extends MProvider { return { list, hasNextPage } } - async getPopular(page) { var types = this.getPreference("animekai_popular_latest_type") return await this.searchPage({ sort: "trending", type: types, page: page }); } - get supportsLatest() { - throw new Error("supportsLatest not implemented"); - } + async getLatestUpdates(page) { var types = this.getPreference("animekai_popular_latest_type") return await this.searchPage({ sort: "updated_date", type: types, page: page }); } + async search(query, page, filters) { function getFilter(state) { var rd = [] @@ -164,7 +156,7 @@ class DefaultExtension extends MProvider { var chapters = [] var animeId = body.selectFirst("#anime-rating").attr("data-id") - var token = await this.generateToken(animeId) + var token = await this.kaiEncrypt(animeId) var res = await this.request(`/ajax/episodes/list?ani_id=${animeId}&_=${token}`) body = JSON.parse(res) if (body.status == 200) { @@ -224,22 +216,77 @@ class DefaultExtension extends MProvider { chapters.reverse() return { name, imageUrl, link, description, genre, status, 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) { - throw new Error("getPageList not implemented"); + var streams = [] + + var epSlug = url.split("||") + + // the 1st time the loop runs its for censored version + var isUncensoredVersion = false + for (var epId of epSlug) { + + var token = await this.kaiEncrypt(epId) + var res = await this.request(`/ajax/links/list?token=${epId}&_=${token}`) + var body = JSON.parse(res) + if (body.status != 200) continue + + var serverResult = new Document(body.result) + + // [{"serverName":"Server 1","dataId":"","dubType":"sub"},{"serverName":"Server 2","dataId":"","dubType":"softsub"}] + var SERVERDATA = [] + // Gives 2 server for each Sub, softsub, dub + var server_items = serverResult.select("div.server-items") + + for (var dubSection of server_items) { + var dubType = dubSection.attr("data-id") + dubType = dubType == "sub" ? "hardsub" : dubType + + dubSection.select("span.server").forEach(ser => { + var serverName = ser.text + var dataId = ser.attr("data-lid") + SERVERDATA.push({ + serverName, + dataId, + dubType + }) + }) + + } + + + //SERVERDATA = [{ "serverName": "Server 1", "dataId": "", "dubType": "hardsub" }]... + for (var serverData of SERVERDATA) { + var serverName = serverData.serverName + var dataId = serverData.dataId + var dubType = serverData.dubType.toUpperCase() + var megaUrl = await this.getMegaUrl(dataId) + + dubType = isUncensoredVersion ? `${dubType} [Uncensored]`:dubType + + var serverStreams = await this.decryptMegaEmbed(megaUrl, serverName, dubType) + streams = [...streams, ...serverStreams] + + // Dubs have subtitles separately, so we need to fetch them too + if (dubType.includes("DUB")) { + if (!megaUrl.includes("sub.list=")) continue; + var subList = megaUrl.split("sub.list=")[1] + + var subres = await this.client.get(subList) + var subtitles = JSON.parse(subres.body) + var subs = this.formatSubtitles(subtitles, dubType) + streams[streams.length - 1].subtitles = subs; + } + } + // the 2nd time the loop runs its for uncensored version + isUncensoredVersion = true; + /// main for end + } + + return streams } + getFilterList() { function formateState(type_name, items, values) { var state = []; @@ -363,6 +410,7 @@ class DefaultExtension extends MProvider { return filters; } + getSourcePreferences() { return [ { @@ -404,60 +452,111 @@ class DefaultExtension extends MProvider { ] } + // ------------------------------- + formatSubtitles(subtitles, dubType) { + var subs = [] + subtitles.forEach(sub => { + if (!sub.kind.includes("thumbnail")) { + subs.push({ + file: sub.file, + label: `${sub.label} - ${dubType}` + }) + } + }) + + return subs + } + + async getMegaUrl(vidId) { + var token = await this.kaiEncrypt(vidId) + var res = await this.request(`/ajax/links/view?id=${vidId}&_=${token}`) + var body = JSON.parse(res) + if (body.status != 200) return + var outEnc = body.result + var out = await this.kaiDecrypt(outEnc) + var o = JSON.parse(out) + return decodeURIComponent(o.url) + } + + async decryptMegaEmbed(megaUrl, serverName, dubType) { + var streams = [] + megaUrl = megaUrl.replace("/e/", "/media/") + var res = await this.client.get(megaUrl) + var body = JSON.parse(res.body) + if (body.status != 200) return + var outEnc = body.result + var streamData = await this.megaDecrypt(outEnc) + var url = streamData.sources[0].file + streams.push({ + url: url, + originalUrl: url, + quality: `Auto - ${dubType} : ${serverName}` + }) + + var subtitles = streamData.tracks + streams[0].subtitles = this.formatSubtitles(subtitles, dubType) + return streams + } + //----------------AnimeKai Decoders---------------- // Credits :- https://github.com/amarullz/kaicodex/ - base64Decoder(base64) { - const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; - let binary = ''; + base64UrlDecode(input) { + let base64 = input + .replace(/-/g, "+") + .replace(/_/g, "/"); - base64 = base64.replace(/=+$/, ''); - - for (let i = 0; i < base64.length; i++) { - const index = chars.indexOf(base64[i]); - if (index === -1) continue; // skip invalid characters - binary += index.toString(2).padStart(6, '0'); + while (base64.length % 4 !== 0) { + base64 += "="; } - let decoded = ''; - for (let i = 0; i < binary.length; i += 8) { - const byte = binary.substring(i, i + 8); - if (byte.length < 8) continue; - decoded += String.fromCharCode(parseInt(byte, 2)); + 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); } - return decoded; + // Convert bytes to ISO-8859-1 string + return String.fromCharCode(...outputBytes); } - base64Encoder(str) { - const base64EncodeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - var out, i, len; - var c1, c2, c3; - len = str.length; - i = 0; - out = ""; - while (i < len) { - c1 = str.charCodeAt(i++) & 0xff; - if (i == len) { - out += base64EncodeChars.charAt(c1 >> 2); - out += base64EncodeChars.charAt((c1 & 0x3) << 4); - out += "=="; - break; - } - c2 = str.charCodeAt(i++); - if (i == len) { - out += base64EncodeChars.charAt(c1 >> 2); - out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); - out += base64EncodeChars.charAt((c2 & 0xF) << 2); - out += "="; - break; - } - c3 = str.charCodeAt(i++); - out += base64EncodeChars.charAt(c1 >> 2); - out += base64EncodeChars.charAt(((c1 & 0x3) << 4) | ((c2 & 0xF0) >> 4)); - out += base64EncodeChars.charAt(((c2 & 0xF) << 2) | ((c3 & 0xC0) >> 6)); - out += base64EncodeChars.charAt(c3 & 0x3F); + + base64UrlEncode(str) { + // Convert to ISO-8859-1 byte array + const bytes = []; + for (let i = 0; i < str.length; i++) { + bytes.push(str.charCodeAt(i) & 0xFF); } - return out; + + // Base64 alphabet + const base64abc = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + // Manual base64 encoding + let base64 = ""; + for (let i = 0; i < bytes.length; i += 3) { + const b1 = bytes[i]; + const b2 = bytes[i + 1] ?? 0; + const b3 = bytes[i + 2] ?? 0; + + const triplet = (b1 << 16) | (b2 << 8) | b3; + + base64 += base64abc[(triplet >> 18) & 0x3F]; + base64 += base64abc[(triplet >> 12) & 0x3F]; + base64 += i + 1 < bytes.length ? base64abc[(triplet >> 6) & 0x3F] : "="; + base64 += i + 2 < bytes.length ? base64abc[triplet & 0x3F] : "="; + } + + // URL-safe Base64 + return base64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } transform(key, text) { @@ -517,18 +616,30 @@ class DefaultExtension extends MProvider { logic.forEach(step => { var method = step[0] if (method == "urlencode") result = encodeURIComponent(result); + else if (method == "urldecode") result = decodeURIComponent(result); else if (method == "rc4") result = this.transform(step[1], result); else if (method == "reverse") result = this.reverseString(result); else if (method == "substitute") result = this.substitute(result, step[1], step[2]); - else if (method == "safeb64_decode") result = this.base64Decoder(result); - else if (method == "safeb64_encode") result = this.base64Encoder(result); + else if (method == "safeb64_decode") result = this.base64UrlDecode(result); + else if (method == "safeb64_encode") result = this.base64UrlEncode(result); }) return result } - async generateToken(id) { + async kaiEncrypt(id) { var token = await this.patternExecutor("kai", "encrypt", id) return token; } -} + async kaiDecrypt(id) { + var token = await this.patternExecutor("kai", "decrypt", id) + return token; + } + + async megaDecrypt(data) { + var streamData = await this.patternExecutor("megaup", "decrypt", data) + return JSON.parse(streamData); + } + + +} \ No newline at end of file From 37a36ceb38ec6ef5913fb0cbc52cfc2088ae3e59 Mon Sep 17 00:00:00 2001 From: Swakshan Date: Thu, 17 Apr 2025 22:36:24 +0530 Subject: [PATCH 07/10] anime(animekai); fixes --- javascript/anime/src/en/animekai.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 4756588b..34c04156 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.2.0", + "version": "0.2.1", "pkgPath": "anime/src/en/animekai.js" }]; @@ -26,7 +26,9 @@ class DefaultExtension extends MProvider { } async request(slug) { - var url = this.getBaseUrl() + slug; + var url = slug + var baseUrl = this.getBaseUrl() + if (!slug.includes(baseUrl)) url = baseUrl + slug; var res = await this.client.get(url); return res.body } @@ -41,7 +43,7 @@ class DefaultExtension extends MProvider { function bundleSlug(category, items) { var rd = "" for (var item of items) { - rd += `&${category}[]=${item}`; + rd += `&${category}[]=${item.toLowerCase()}`; } return rd; } @@ -58,7 +60,7 @@ class DefaultExtension extends MProvider { slug += bundleSlug("rating", rating); slug += bundleSlug("country", country); slug += bundleSlug("language", language); - sort = sort.length < 1 ? "most_relevance" : "" // default sort is most relevance + sort = sort.length < 1 ? "most_relevance" : sort// default sort is most relevance slug += "&sort=" + sort; slug += `&page=${page}`; @@ -222,7 +224,7 @@ class DefaultExtension extends MProvider { var streams = [] var epSlug = url.split("||") - + // the 1st time the loop runs its for censored version var isUncensoredVersion = false for (var epId of epSlug) { @@ -263,7 +265,7 @@ class DefaultExtension extends MProvider { var dubType = serverData.dubType.toUpperCase() var megaUrl = await this.getMegaUrl(dataId) - dubType = isUncensoredVersion ? `${dubType} [Uncensored]`:dubType + dubType = isUncensoredVersion ? `${dubType} [Uncensored]` : dubType var serverStreams = await this.decryptMegaEmbed(megaUrl, serverName, dubType) streams = [...streams, ...serverStreams] From 75f81721379b0383103dba764fd6c106de7d0f6c Mon Sep 17 00:00:00 2001 From: Swakshan Date: Thu, 17 Apr 2025 23:18:42 +0530 Subject: [PATCH 08/10] anime(animekai): split streams preference --- javascript/anime/src/en/animekai.js | 51 +++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 34c04156..96e50e92 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.2.1", + "version": "0.2.2", "pkgPath": "anime/src/en/animekai.js" }]; @@ -450,6 +450,13 @@ class DefaultExtension extends MProvider { summary: "", value: true } + }, { + key: "animekai_pref_extract_streams", + switchPreferenceCompat: { + title: 'Split stream into different quality streams', + summary: "Split stream Auto into 360p/720p/1080p", + value: true + } }, ] } @@ -469,6 +476,41 @@ class DefaultExtension extends MProvider { return subs } + async formatStreams(sUrl, serverName, dubType) { + function streamNamer(res) { + return `${res} - ${dubType} : ${serverName}` + } + + var streams = [{ + url: sUrl, + originalUrl: sUrl, + quality: streamNamer("Auto") + }] + + var pref = this.getPreference("animekai_pref_extract_streams") + if (!pref) return streams + + var baseUrl = sUrl.split("/list.m3u8")[0].split("/list,")[0] + + const response = await new Client().get(sUrl); + const body = response.body; + const lines = body.split('\n'); + + for (let i = 0; i < lines.length; i++) { + if (lines[i].startsWith('#EXT-X-STREAM-INF:')) { + var resolution = lines[i].match(/RESOLUTION=(\d+x\d+)/)[1]; + var qUrl = lines[i + 1].trim(); + var m3u8Url = `${baseUrl}/${qUrl}` + streams.push({ + url: m3u8Url, + originalUrl: m3u8Url, + quality: streamNamer(resolution) + }); + } + } + return streams + } + async getMegaUrl(vidId) { var token = await this.kaiEncrypt(vidId) var res = await this.request(`/ajax/links/view?id=${vidId}&_=${token}`) @@ -489,11 +531,8 @@ class DefaultExtension extends MProvider { var outEnc = body.result var streamData = await this.megaDecrypt(outEnc) var url = streamData.sources[0].file - streams.push({ - url: url, - originalUrl: url, - quality: `Auto - ${dubType} : ${serverName}` - }) + + var streams =await this.formatStreams(url, serverName, dubType) var subtitles = streamData.tracks streams[0].subtitles = this.formatSubtitles(subtitles, dubType) From fc66a78b3fbcb69dc7384cbe903251148e16986a Mon Sep 17 00:00:00 2001 From: Swakshan Date: Fri, 18 Apr 2025 11:47:59 +0530 Subject: [PATCH 09/10] anime(animekai): Added server & sub/dub type preference --- javascript/anime/src/en/animekai.js | 50 ++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index 96e50e92..a5b206f3 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -6,7 +6,7 @@ const mangayomiSources = [{ "iconUrl": "https://www.google.com/s2/favicons?sz=256&domain=https://animekai.to/", "typeSource": "single", "itemType": 1, - "version": "0.2.2", + "version": "0.2.3", "pkgPath": "anime/src/en/animekai.js" }]; @@ -222,10 +222,17 @@ class DefaultExtension extends MProvider { // For anime episode video list async getVideoList(url) { var streams = [] + var prefServer = this.getPreference("animekai_pref_stream_server") + // If no server is chosen, use the default server 1 + if (prefServer.length < 1) prefServer.push("1") + + var prefDubType = this.getPreference("animekai_pref_stream_subdub_type") + // If no dubtype is chosen, use the default dubtype sub + if (prefDubType.length < 1) prefDubType.push("sub") var epSlug = url.split("||") - // the 1st time the loop runs its for censored version + // The 1st time the loop runs its for censored version var isUncensoredVersion = false for (var epId of epSlug) { @@ -243,30 +250,33 @@ class DefaultExtension extends MProvider { for (var dubSection of server_items) { var dubType = dubSection.attr("data-id") - dubType = dubType == "sub" ? "hardsub" : dubType + // If dubtype is not in preference dont include it + if (!prefDubType.includes(dubType)) continue - dubSection.select("span.server").forEach(ser => { + for (var ser of dubSection.select("span.server")) { var serverName = ser.text + // If servername is not in preference dont include it + if (!prefServer.includes(serverName.replace("Server ", ""))) continue + var dataId = ser.attr("data-lid") SERVERDATA.push({ serverName, dataId, dubType }) - }) + } } - //SERVERDATA = [{ "serverName": "Server 1", "dataId": "", "dubType": "hardsub" }]... for (var serverData of SERVERDATA) { var serverName = serverData.serverName var dataId = serverData.dataId var dubType = serverData.dubType.toUpperCase() - var megaUrl = await this.getMegaUrl(dataId) - + dubType = dubType == "SUB" ? "HARDSUB" : dubType dubType = isUncensoredVersion ? `${dubType} [Uncensored]` : dubType + var megaUrl = await this.getMegaUrl(dataId) var serverStreams = await this.decryptMegaEmbed(megaUrl, serverName, dubType) streams = [...streams, ...serverStreams] @@ -281,9 +291,9 @@ class DefaultExtension extends MProvider { streams[streams.length - 1].subtitles = subs; } } - // the 2nd time the loop runs its for uncensored version + // The 2nd time the loop runs its for uncensored version isUncensoredVersion = true; - /// main for end + // Main for ends } return streams @@ -450,6 +460,24 @@ class DefaultExtension extends MProvider { summary: "", value: true } + }, { + key: "animekai_pref_stream_server", + multiSelectListPreference: { + title: 'Preferred server', + summary: 'Choose the server/s you want to extract streams from', + values: ["1"], + entries: ["Server 1", "Server 2"], + entryValues: ["1", "2"] + } + }, { + key: "animekai_pref_stream_subdub_type", + multiSelectListPreference: { + title: 'Preferred stream sub/dub type', + summary: '', + values: ["sub", "softsub", "dub"], + entries: ["Hard Sub", "Soft Sub", "Dub"], + entryValues: ["sub", "softsub", "dub"] + } }, { key: "animekai_pref_extract_streams", switchPreferenceCompat: { @@ -532,7 +560,7 @@ class DefaultExtension extends MProvider { var streamData = await this.megaDecrypt(outEnc) var url = streamData.sources[0].file - var streams =await this.formatStreams(url, serverName, dubType) + var streams = await this.formatStreams(url, serverName, dubType) var subtitles = streamData.tracks streams[0].subtitles = this.formatSubtitles(subtitles, dubType) From fdeb48a39986c7753f18de1a1a057c93dafa4d9e Mon Sep 17 00:00:00 2001 From: Swakshan <56347042+Swakshan@users.noreply.github.com> Date: Fri, 18 Apr 2025 14:52:07 +0530 Subject: [PATCH 10/10] anime(animekai): Change default sort to "updated date" --- javascript/anime/src/en/animekai.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/javascript/anime/src/en/animekai.js b/javascript/anime/src/en/animekai.js index a5b206f3..9a9dbdc0 100644 --- a/javascript/anime/src/en/animekai.js +++ b/javascript/anime/src/en/animekai.js @@ -60,7 +60,7 @@ class DefaultExtension extends MProvider { slug += bundleSlug("rating", rating); slug += bundleSlug("country", country); slug += bundleSlug("language", language); - sort = sort.length < 1 ? "most_relevance" : sort// default sort is most relevance + sort = sort.length < 1 ? "updated_date" : sort// default sort is updated date slug += "&sort=" + sort; slug += `&page=${page}`; @@ -711,4 +711,4 @@ class DefaultExtension extends MProvider { } -} \ No newline at end of file +}