From 65b86799e520e144637a96dbf39e0c50432842f4 Mon Sep 17 00:00:00 2001 From: kodjomoustapha <107993382+kodjodevf@users.noreply.github.com> Date: Thu, 28 Mar 2024 11:15:21 +0100 Subject: [PATCH] feat:JS new sourcces AniWorld(DE) & AllAnime(EN) --- javascript/anime/src/de/aniworld.js | 252 +++++++++++++++ javascript/anime/src/en/allanime.js | 482 ++++++++++++++++++++++++++++ javascript/icon/de.aniworld.png | Bin 0 -> 4090 bytes javascript/icon/en.allanime.png | Bin 0 -> 5389 bytes 4 files changed, 734 insertions(+) create mode 100644 javascript/anime/src/de/aniworld.js create mode 100644 javascript/anime/src/en/allanime.js create mode 100644 javascript/icon/de.aniworld.png create mode 100644 javascript/icon/en.allanime.png diff --git a/javascript/anime/src/de/aniworld.js b/javascript/anime/src/de/aniworld.js new file mode 100644 index 00000000..2766add7 --- /dev/null +++ b/javascript/anime/src/de/aniworld.js @@ -0,0 +1,252 @@ +const mangayomiSources = [{ + "name": "AniWorld", + "lang": "de", + "baseUrl": "https://aniworld.to", + "apiUrl": "", + "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/de.aniworld.png", + "typeSource": "single", + "isManga": false, + "isNsfw": false, + "version": "0.0.1", + "dateFormat": "", + "dateFormatLocale": "", + "pkgPath": "anime/src/de/aniworld.js" +}]; + +class DefaultExtension extends MProvider { + async getPopular(page) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get(`${baseUrl}/beliebte-animes`); + const elements = new Document(res.body).select("div.seriesListContainer div"); + const list = []; + for (const element of elements) { + const linkElement = element.selectFirst("a"); + const name = element.selectFirst("h3").text; + const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src"); + const link = linkElement.attr("href"); + list.push({ name, imageUrl, link }); + } + return { + list: list, + hasNextPage: false + } + } + async getLatestUpdates(page) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get(`${baseUrl}/neu`); + const elements = new Document(res.body).select("div.seriesListContainer div"); + const list = []; + for (const element of elements) { + const linkElement = element.selectFirst("a"); + const name = element.selectFirst("h3").text; + const imageUrl = baseUrl + linkElement.selectFirst("img").attr("data-src"); + const link = linkElement.attr("href"); + list.push({ name, imageUrl, link }); + } + return { + list: list, + hasNextPage: false + } + } + async search(query, page, filters) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get(`${baseUrl}/animes`); + const elements = new Document(res.body).select("#seriesContainer > div > ul > li > a").filter(e => e.attr("title").toLowerCase().includes(query.toLowerCase())); + const list = []; + for (const element of elements) { + const name = element.text; + const link = element.attr("href"); + const img = new Document((await new Client().get(baseUrl + link)).body).selectFirst("div.seriesCoverBox img").attr("data-src"); + const imageUrl = baseUrl + img; + list.push({ name, imageUrl, link }); + } + return { + list: list, + hasNextPage: false + } + } + async getDetail(url) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get(baseUrl + url); + const document = new Document(res.body); + const imageUrl = baseUrl + + document.selectFirst("div.seriesCoverBox img").attr("data-src"); + const name = document.selectFirst("div.series-title h1 span").text; + const genre = document.select("div.genres ul li").map(e => e.text); + const description = document.selectFirst("p.seri_des").attr("data-full-description"); + const produzent = document.select("div.cast li") + .filter(e => e.outerHtml.includes("Produzent:")); + let author = ""; + if (produzent.length > 0) { + author = produzent[0].select("li").map(e => e.text).join(", "); + } + const seasonsElements = document.select("#stream > ul:nth-child(1) > li > a"); + let episodes = []; + for (const element of seasonsElements) { + const eps = await this.parseEpisodesFromSeries(element); + for (const ep of eps) { + episodes.push(ep); + } + } + episodes.reverse(); + + return { + name, imageUrl, description, author, status: 5, genre, episodes + }; + } + async parseEpisodesFromSeries(element) { + const seasonId = element.getHref; + const res = await new Client().get(this.source.baseUrl + seasonId); + const episodeElements = new Document(res.body).select("table.seasonEpisodesList tbody tr"); + const list = []; + for (const episodeElement of episodeElements) { + list.push(this.episodeFromElement(episodeElement)); + } + return list; + } + episodeFromElement(element) { + let name = ""; + let url = ""; + if (element.selectFirst("td.seasonEpisodeTitle a").attr("href").includes("/film")) { + const num = element.attr("data-episode-season-id"); + name = `Film ${num}` + " : " + element.selectFirst("td.seasonEpisodeTitle a span").text; + url = element.selectFirst("td.seasonEpisodeTitle a").attr("href"); + } else { + const season = + element.selectFirst("td.seasonEpisodeTitle a").attr("href").substringAfter("staffel-").substringBefore("/episode");; + const num = element.attr("data-episode-season-id"); + name = `Staffel ${season} Folge ${num}` + " : " + element.selectFirst("td.seasonEpisodeTitle a span").text; + url = element.selectFirst("td.seasonEpisodeTitle a").attr("href"); + } + if (name.length > 0 && url.length > 0) { + return { name, url } + } + return {} + } + async getVideoList(url) { + const baseUrl = this.source.baseUrl; + const res = await new Client().get(baseUrl + url); + const document = new Document(res.body); + const redirectlink = document.select("ul.row li"); + const preference = new SharedPreferences(); + const hosterSelection = preference.get("hoster_selection"); + const videos = []; + for (const element of redirectlink) { + const langkey = element.attr("data-lang-key"); + let language = ""; + if (langkey.includes("3")) { + language = "Deutscher Sub"; + } else if (langkey.includes("1")) { + language = "Deutscher Dub"; + } else if (langkey.includes("2")) { + language = "Englischer Sub"; + } + const redirectgs = baseUrl + element.selectFirst("a.watchEpisode").attr("href"); + const hoster = element.selectFirst("a h4").text; + if (hoster == "Streamtape" && hosterSelection.includes("Streamtape")) { + const body = (await new Client().get(redirectgs)).body; + const quality = `Streamtape ${language}`; + const vids = await streamTapeExtractor(body.match(/https:\/\/streamtape\.com\/e\/[a-zA-Z0-9]+/g)[0], quality); + for (const vid of vids) { + videos.push(vid); + } + } else if (hoster == "VOE" && hosterSelection.includes("VOE")) { + const body = (await new Client().get(redirectgs)).body; + const quality = `VOE ${language}`; + const vids = await voeExtractor(body.match(/https:\/\/voe\.sx\/e\/[a-zA-Z0-9]+/g)[0], quality); + for (const vid of vids) { + videos.push(vid); + } + } else if (hoster == "Vidoza" && hosterSelection.includes("Vidoza")) { + const body = (await new Client().get(redirectgs)).body; + const quality = `Vidoza ${language}`; + const match = body.match(/https:\/\/[^\s]*\.vidoza\.net\/[^\s]*\.mp4/g); + if (match.length > 0) { + videos.push({ url: match[0], originalUrl: match[0], quality }); + } + } + } + return this.sortVideos(videos); + } + sortVideos(videos) { + const preference = new SharedPreferences(); + const hoster = preference.get("preferred_hoster"); + const subPreference = preference.get("preferred_lang"); + videos.sort((a, b) => { + let qualityMatchA = 0; + if (a.quality.includes(hoster) && + a.quality.includes(subPreference)) { + qualityMatchA = 1; + } + let qualityMatchB = 0; + if (b.quality.includes(hoster) && + b.quality.includes(subPreference)) { + qualityMatchB = 1; + } + return qualityMatchB - qualityMatchA; + }); + return videos; + } + getSourcePreferences() { + return [ + { + "key": "preferred_lang", + "listPreference": { + "title": "Bevorzugte Sprache", + "summary": "", + "valueIndex": 0, + "entries": [ + "Deutscher Sub", + "Deutscher Dub", + "Englischer Sub" + ], + "entryValues": [ + "Deutscher Sub", + "Deutscher Dub", + "Englischer Sub" + ] + } + }, + { + "key": "preferred_hoster", + "listPreference": { + "title": "Standard-Hoster", + "summary": "", + "valueIndex": 0, + "entries": [ + "Streamtape", + "VOE", + "Vidoza" + ], + "entryValues": [ + "Streamtape", + "VOE", + "Vidoza" + ] + } + }, + { + "key": "hoster_selection", + "multiSelectListPreference": { + "title": "Hoster auswählen", + "summary": "", + "entries": [ + "Streamtape", + "VOE", + "Vidoza" + ], + "entryValues": [ + "Streamtape", + "VOE", + "Vidoza" + ], + "values": [ + "Streamtape", + "VOE", + "Vidoza" + ] + } + } + ]; + } +} diff --git a/javascript/anime/src/en/allanime.js b/javascript/anime/src/en/allanime.js new file mode 100644 index 00000000..723dc4f9 --- /dev/null +++ b/javascript/anime/src/en/allanime.js @@ -0,0 +1,482 @@ +const mangayomiSources = [{ + "name": "AllAnime", + "lang": "en", + "baseUrl": "https://allanime.to", + "apiUrl": "https://api.allanime.day/api", + "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/en.allanime.png", + "typeSource": "single", + "isManga": false, + "isNsfw": false, + "version": "0.0.1", + "apiUrl": "", + "dateFormat": "", + "dateFormatLocale": "", + "pkgName": "anime/src/en/allanime.js" +}]; + +class DefaultExtension extends MProvider { + async request(body) { + const apiUrl = this.source.apiUrl; + const baseUrl = this.source.baseUrl; + return (await new Client().get(apiUrl + body, { "Referer": baseUrl })).body + } + async getPopular(page) { + const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22type%22:%20%22anime%22,%0A%20%20%20%20%20%20%20%20%20%20%22size%22:%2026,%0A%20%20%20%20%20%20%20%20%20%20%22dateRange%22:%201,%0A%20%20%20%20%20%20%20%20%20%20%22page%22:%20${page}%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($type:%20VaildPopularTypeEnumType!,%20$size:%20Int!,%20$dateRange:%20Int,%20$page:%20Int)%20%7B%0A%20%20%20%20%20%20%20%20%20%20queryPopular(type:%20$type,%20size:%20$size,%20dateRange:%20$dateRange,%20page:%20$page)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20recommendations%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20anyCard%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20_id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20englishName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20nativeName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20slugTime%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20` + const resList = JSON.parse(await this.request(encodedGql)).data.queryPopular.recommendations.filter(e => e.anyCard !== null); + const preferences = new SharedPreferences(); + const titleStyle = preferences.get("preferred_title_style"); + const list = []; + for (const anime of resList) { + let title; + if (titleStyle === "romaji") { + title = anime.anyCard.name; + } else if (titleStyle === "eng") { + title = anime.anyCard.englishName || anime.anyCard.name; + } else { + title = anime.anyCard.nativeName || anime.anyCard.name; + } + const name = title; + const imageUrl = anime.anyCard.thumbnail; + const link = `/bangumi/${anime.anyCard._id}/${anime.anyCard.name.replace(/[^a-zA-Z0-9]/g, "-") + .replace(/-{2,}/g, "-") + .toLowerCase()}`; + list.push({ name, imageUrl, link }); + } + + return { + list: list, + hasNextPage: list.length === 26 + } + } + + async getLatestUpdates(page) { + return await this.search("", page, []); + } + async search(query, page, filters) { + query = query.replace(" ", "%20"); + const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22search%22:%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%22query%22:%20%22${query}%22,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22allowAdult%22:%20false,%0A%20%20%20%20%20%20%20%20%20%20%20%20%22allowUnknown%22:%20false%0A%20%20%20%20%20%20%20%20%20%20%7D,%0A%20%20%20%20%20%20%20%20%20%20%22countryOrigin%22:%20%22ALL%22,%0A%20%20%20%20%20%20%20%20%20%20%22limit%22:%2026,%0A%20%20%20%20%20%20%20%20%20%20%22page%22:%20${page}%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($search:%20SearchInput,%20$limit:%20Int,%20$countryOrigin:%20VaildCountryOriginEnumType,%20$page:%20Int)%20%7B%0A%20%20%20%20%20%20%20%20%20%20shows(search:%20$search,%20limit:%20$limit,%20countryOrigin:%20$countryOrigin,%20page:%20$page)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20edges%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20_id%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20name%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20nativeName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20englishName%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20slugTime%0A%20%20%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`; + const resList = JSON.parse(await this.request(encodedGql)).data.shows.edges; + const preferences = new SharedPreferences(); + const titleStyle = preferences.get("preferred_title_style"); + const list = []; + for (const anime of resList) { + let title; + if (titleStyle === "romaji") { + title = anime.name; + } else if (titleStyle === "eng") { + title = anime.englishName || anime.name; + } else { + title = anime.nativeName || anime.name; + } + const name = title; + const imageUrl = anime.thumbnail; + const link = `/bangumi/${anime._id}/${anime.name.replace(/[^a-zA-Z0-9]/g, "-") + .replace(/-{2,}/g, "-") + .toLowerCase()}`; + list.push({ name, imageUrl, link }); + } + + return { + list: list, + hasNextPage: list.length === 26 + } + } + async getDetail(url) { + const id = url.substringAfter('bangumi/').substringBefore('/'); + const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22id%22:%20%22${id}%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query($id:%20String!)%20%7B%0A%20%20%20%20%20%20%20%20%20%20show(_id:%20$id)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20thumbnail%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20description%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20type%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20season%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20score%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20genres%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20status%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20studios%0A%20%20%20%20%20%20%20%20%20%20%20%20%20%20availableEpisodesDetail%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`; + const show = JSON.parse(await this.request(encodedGql)).data.show; + const genre = show.genres || []; + const status = this.parseStatus(show.status); + const author = show.studios.length > 0 ? show.studios[0] : ""; + let description = ""; + description = description.concat(show.description, "\n\n", `Type: ${show.type || "Unknown"}`, `\nAired: ${show.season?.quarter || "-"} ${show.season?.year || "-"}`, `\nScore: ${show.score || "-"}★`); + let episodesSub = []; + for (const episode of show.availableEpisodesDetail.sub) { + const num = parseInt(episode) || 1; + const name = `Episode ${num}`; + const url = JSON.stringify({ + showId: id, + translationType: ["sub"], + episodeString: episode + }); + const scanlator = "sub"; + episodesSub.push({ num, name, url, scanlator }); + } + let episodesDub = []; + for (const episode of show.availableEpisodesDetail.dub) { + const num = parseInt(episode) || 1; + const name = `Episode ${num}`; + const url = JSON.stringify({ + showId: id, + translationType: ["dub"], + episodeString: episode + }); + const scanlator = "dub"; + episodesDub.push({ num, name, url, scanlator }); + } + let episodes = []; + if (episodesSub.length > 0 && episodesSub.length) { + episodes = episodesSub.map(ep => { + const f = episodesDub.filter(e => e.num === ep.num); + if (f.length > 0) { + const url = JSON.parse(ep.url); + return { + "name": ep.name, "url": JSON.stringify({ + showId: url.showId, + translationType: ['sub', 'dub'], + episodeString: url.episodeString + }), scanlator: `sub, dub` + } + } + else { + return ep; + } + }) + } else { + episodes = episodesDub; + } + return { + description, author, status, genre, episodes + } + } + parseStatus(string) { + switch (string) { + case "Releasing": + return 0; + case "Finished": + return 1; + case "Not Yet Released": + return 0; + default: + return 5; + } + } + async getVideoList(url) { + const baseUrl = this.source.baseUrl; + const preferences = new SharedPreferences(); + const subPref = preferences.get("preferred_sub"); + const ep = JSON.parse(url); + const translationType = ep.translationType.filter(t => t === subPref); + if (translationType.length == 0) { + return []; + } + const encodedGql = `?variables=%0A%20%20%20%20%20%20%20%20%7B%0A%20%20%20%20%20%20%20%20%20%20%22showId%22:%20%22${ep.showId}%22,%0A%20%20%20%20%20%20%20%20%20%20%22episodeString%22:%20%22${ep.episodeString}%22,%0A%20%20%20%20%20%20%20%20%20%20%22translationType%22:%20%22${translationType[0]}%22%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20&query=%0A%20%20%20%20%20%20%20%20query(%0A%20%20%20%20%20%20%20%20%20%20$showId:%20String!%0A%20%20%20%20%20%20%20%20%20%20$episodeString:%20String!%0A%20%20%20%20%20%20%20%20%20%20$translationType:%20VaildTranslationTypeEnumType!%0A%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20episode(%0A%20%20%20%20%20%20%20%20%20%20%20%20showId:%20$showId%0A%20%20%20%20%20%20%20%20%20%20%20%20episodeString:%20$episodeString%0A%20%20%20%20%20%20%20%20%20%20%20%20translationType:%20$translationType%0A%20%20%20%20%20%20%20%20%20%20)%20%7B%0A%20%20%20%20%20%20%20%20%20%20%20%20sourceUrls%0A%20%20%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20%20%20`; + const videoJson = JSON.parse(await this.request(encodedGql)); + const videos = []; + const altHosterSelection = preferences.get('alt_hoster_selection'); + for (const video of videoJson.data.episode.sourceUrls) { + const videoUrl = this.decryptSource(video.sourceUrl); + let quality = ""; + if (videoUrl.includes("/apivtwo/") && altHosterSelection.some(element => 'player' === element)) { + quality = `internal ${video.sourceName}`; + const vids = await new AllAnimeExtractor({ "Referer": baseUrl }, "https://allanime.to").videoFromUrl(videoUrl, quality); + for (const vid of vids) { + videos.push(vid); + } + } else if (["vidstreaming", "https://gogo", "playgo1.cc", "playtaku"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'vidstreaming' === element)) { + const vids = await gogoCdnExtractor(videoUrl); + for (const vid of vids) { + videos.push(vid); + } + } else if (["dood", "d0"].some(element => videoUrl.includes(element)) && altHosterSelection.some(element => 'dood' === element)) { + const vids = await doodExtractor(videoUrl); + for (const vid of vids) { + videos.push(vid); + } + } else if (videoUrl.includes("ok.ru") && altHosterSelection.some(element => 'okru' === element)) { + const vids = await okruExtractor(videoUrl); + for (const vid of vids) { + videos.push(vid); + } + } else if (videoUrl.includes("mp4upload.com") && altHosterSelection.some(element => 'mp4upload' === element)) { + const vids = await mp4UploadExtractor(videoUrl); + for (const vid of vids) { + videos.push(vid); + } + } else if (videoUrl.includes("streamlare.com") && altHosterSelection.some(element => 'streamlare' === element)) { + const vids = await streamlareExtractor(videoUrl); + for (const vid of vids) { + videos.push(vid); + } + } + } + return this.sortVideos(videos); + } + sortVideos(videos) { + const preferences = new SharedPreferences(); + const hoster = preferences.get("preferred_hoster"); + const quality = preferences.get("preferred_quality"); + videos.sort((a, b) => { + let qualityMatchA = 0; + if (a.quality.includes(hoster) && + a.quality.includes(quality)) { + qualityMatchA = 1; + } + let qualityMatchB = 0; + if (b.quality.includes(hoster) && + b.quality.includes(quality)) { + qualityMatchB = 1; + } + return qualityMatchB - qualityMatchA; + }); + return videos; + } + decryptSource(str) { + if (str.startsWith("-")) { + return str.substring(str.lastIndexOf('-') + 1) + .match(/.{1,2}/g) + .map(hex => parseInt(hex, 16)) + .map(byte => String.fromCharCode(byte ^ 56)) + .join(""); + } else { + return str; + } + } + getSourcePreferences() { + return [ + { + "key": "preferred_title_style", + "listPreference": { + "title": "Preferred Title Style", + "summary": "", + "valueIndex": 0, + "entries": ["Romaji", "English", "Native"], + "entryValues": ["romaji", "eng", "native"] + } + }, + { + "key": "preferred_quality", + "listPreference": { + "title": "Preferred quality", + "summary": "", + "valueIndex": 0, + "entries": [ + "2160p", + "1440p", + "1080p", + "720p", + "480p", + "360p", + "240p", + "80p"], + "entryValues": [ + "2160", + "1440", + "1080", + "720", + "480", + "360", + "240", + "80"] + } + }, + { + "key": "preferred_sub", + "listPreference": { + "title": "Prefer subs or dubs?", + "summary": "", + "valueIndex": 0, + "entries": ["Subs", "Dubs"], + "entryValues": ["sub", "dub"] + } + }, + { + "key": "preferred_hoster", + "listPreference": { + "title": "Preferred Video Server", + "summary": "", + "valueIndex": 0, + "entries": [ + "Ac", "Ak", "Kir", "Rab", "Luf-mp4", + "Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls", + "vidstreaming", "okru", "mp4upload", "streamlare", "doodstream" + ], + "entryValues": [ + "Ac", "Ak", "Kir", "Rab", "Luf-mp4", + "Si-Hls", "S-mp4", "Ac-Hls", "Uv-mp4", "Pn-Hls", + "vidstreaming", "okru", "mp4upload", "streamlare", "doodstream" + ] + } + }, + { + "key": "alt_hoster_selection", + "multiSelectListPreference": { + "title": "Enable/Disable Alternative Hosts", + "summary": "", + "entries": [ + "player", + "vidstreaming", + "okru", + "mp4upload", + "streamlare", + "doodstream" + ], + "entryValues": [ + "player", + "vidstreaming", + "okru", + "mp4upload", + "streamlare", + "doodstream" + ], + "values": [ + "player", + "vidstreaming", + "okru", + "mp4upload", + "streamlare", + "doodstream" + ] + } + } + ]; + } +} + +class AllAnimeExtractor { + constructor(headers, baseUrl) { + this.headers = headers; + this.baseUrl = baseUrl; + } + + bytesIntoHumanReadable(bytes) { + const kilobyte = 1000; + const megabyte = kilobyte * 1000; + const gigabyte = megabyte * 1000; + const terabyte = gigabyte * 1000; + + if (bytes >= 0 && bytes < kilobyte) { + return `${bytes} b/s`; + } else if (bytes >= kilobyte && bytes < megabyte) { + return `${Math.floor(bytes / kilobyte)} kb/s`; + } else if (bytes >= megabyte && bytes < gigabyte) { + return `${Math.floor(bytes / megabyte)} mb/s`; + } else if (bytes >= gigabyte && bytes < terabyte) { + return `${Math.floor(bytes / gigabyte)} gb/s`; + } else if (bytes >= terabyte) { + return `${Math.floor(bytes / terabyte)} tb/s`; + } else { + return `${bytes} bits/s`; + } + } + + async videoFromUrl(url, name) { + const videoList = []; + const endPointResponse = JSON.parse((await new Client().get(`${this.baseUrl}/getVersion`, this.headers)).body); + const endPoint = endPointResponse.episodeIframeHead; + + const resp = await new Client().get(endPoint + url.replace("/clock?", "/clock.json?"), this.headers); + + if (resp.statusCode !== 200) { + return []; + } + const linkJson = JSON.parse(resp.body); + for (const link of linkJson.links) { + const subtitles = []; + if (link.subtitles && link.subtitles.length > 0) { + subtitles.push(...link.subtitles.map(sub => { + const label = sub.label ? ` - ${sub.label}` : ''; + return { file: sub.src, label: `${sub.lang}${label}` }; + })); + } + if (link.mp4) { + videoList.push({ + url: + link.link, + quality: `Original (${name} - ${link.resolutionStr})`, + originalUrl: link.link, + subtitles, + }); + } else if (link.hls) { + const headers = + { + 'Host': link.link.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1], + 'Origin': endPoint, + 'Referer': `${endPoint}/` + }; + const resp = await new Client().get(link.link, headers); + + if (resp.statusCode === 200) { + const masterPlaylist = resp.body; + const audios = []; + if (masterPlaylist.includes('#EXT-X-MEDIA:TYPE=AUDIO')) { + const audioInfo = masterPlaylist.substringAfter('#EXT-X-MEDIA:TYPE=AUDIO').substringBefore('\n'); + const language = audioInfo.substringAfter('NAME="').substringBefore('"'); + const url = audioInfo.substringAfter('URI="').substringBefore('"'); + audios.push({ file: url, label: language }); + } + if (!masterPlaylist.includes('#EXT-X-STREAM-INF:')) { + if (audios.length === 0) { + videoList.push({ url: link.link, quality: `${name} - ${link.resolutionStr}`, originalUrl: link.link, subtitles, headers }); + } else { + videoList.push({ url: link.link, quality: `${name} - ${link.resolutionStr}`, originalUrl: link.link, subtitles, audios, headers }); + } + } else { + masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(it => { + let bandwidth = ''; + if (it.includes('AVERAGE-BANDWIDTH')) { + bandwidth = ` ${this.bytesIntoHumanReadable(it.substringAfter('AVERAGE-BANDWIDTH=').substringBefore(','))}`; + } + const quality = `${it.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p${bandwidth} (${name} - ${link.resolutionStr})`; + let videoUrl = it.substringAfter('\n').substringBefore('\n'); + + if (!videoUrl.startsWith('http')) { + videoUrl = resp.request.url.substringBeforeLast('/') + `/${videoUrl}`; + } + const headers = + { + 'Host': videoUrl.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/)[1], + 'Origin': endPoint, + 'Referer': `${endPoint}/` + }; + if (audios.length === 0) { + videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, headers }); + } else { + videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles, audios, headers }); + } + + }); + } + } + } else if (link.crIframe) { + for (const stream of link.portData.streams) { + if (stream.format === 'adaptive_dash') { + videoList.push({ + url: + stream.url, + quality: `Original (AC - Dash${stream.hardsub_lang.length === 0 ? '' : ` - Hardsub: ${stream.hardsub_lang}`})`, + originalUrl: stream.url, + subtitles, + }); + } else if (stream.format === 'adaptive_hls') { + const resp = await new Client().get(stream.url, { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0' }) + if (resp.statusCode === 200) { + const masterPlaylist = resp.body; + masterPlaylist.substringAfter('#EXT-X-STREAM-INF:').split('#EXT-X-STREAM-INF:').forEach(t => { + const quality = `${t.substringAfter('RESOLUTION=').substringAfter('x').substringBefore(',')}p (AC - HLS${stream.hardsub_lang.length === 0 ? '' : ` - Hardsub: ${stream.hardsub_lang}`})`; + const videoUrl = t.substringAfter('\n').substringBefore('\n'); + videoList.push({ url: videoUrl, quality, originalUrl: videoUrl, subtitles }); + }); + } + } + } + } else if (link.dash) { + const audios = link.rawUrls && link.rawUrls.audios ? link.rawUrls.audios.map(it => { return { file: it.url, label: this.bytesIntoHumanReadable(it.bandwidth) }; }) : []; + const videos = link.rawUrls && link.rawUrls.vids ? link.rawUrls.vids.map + (it => { + if (!audios) { + return { url: it.url, quality: `${name} - ${it.height} ${this.bytesIntoHumanReadable(it.bandwidth)}`, originalUrl: it.url, subtitles }; + } else { + return { url: it.url, quality: `${name} - ${it.height} ${this.bytesIntoHumanReadable(it.bandwidth)}`, originalUrl: it.url, audios, subtitles }; + } + }) : []; + + if (videos.length > 0) { + videoList.push(...videos); + } + } + } + return videoList; + } +} + diff --git a/javascript/icon/de.aniworld.png b/javascript/icon/de.aniworld.png new file mode 100644 index 0000000000000000000000000000000000000000..32573e8afaa06a4cdf4bf1e2161db2c0091f8b33 GIT binary patch literal 4090 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 literal 0 HcmV?d00001 diff --git a/javascript/icon/en.allanime.png b/javascript/icon/en.allanime.png new file mode 100644 index 0000000000000000000000000000000000000000..53ee5001a768b4c44cec70cb865ec63acec9494b GIT binary patch literal 5389 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 literal 0 HcmV?d00001