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 00000000..32573e8a Binary files /dev/null and b/javascript/icon/de.aniworld.png differ diff --git a/javascript/icon/en.allanime.png b/javascript/icon/en.allanime.png new file mode 100644 index 00000000..53ee5001 Binary files /dev/null and b/javascript/icon/en.allanime.png differ