diff --git a/javascript/anime/src/all/torrentio.js b/javascript/anime/src/all/torrentio.js
new file mode 100644
index 00000000..b3bbc23e
--- /dev/null
+++ b/javascript/anime/src/all/torrentio.js
@@ -0,0 +1,645 @@
+const mangayomiSources = [{
+ "name": "Torrentio (Torrent)",
+ "lang": "all",
+ "baseUrl": "https://torrentio.strem.fun",
+ "apiUrl": "",
+ "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.torrentio.png",
+ "typeSource": "torrent",
+ "isManga": false,
+ "version": "0.0.1",
+ "appMinVerReq": "0.3.75",
+ "pkgPath": "anime/src/all/torrentio.js"
+}];
+
+class DefaultExtension extends MProvider {
+ constructor() {
+ super();
+ this.client = new Client();
+ }
+
+ justWatchQuery() {
+ return `
+ query GetPopularTitles(
+ $country: Country!,
+ $first: Int!,
+ $language: Language!,
+ $offset: Int,
+ $searchQuery: String,
+ $packages: [String!]!,
+ $objectTypes: [ObjectType!]!,
+ $popularTitlesSortBy: PopularTitlesSorting!,
+ $releaseYear: IntFilter
+ ) {
+ popularTitles(
+ country: $country
+ first: $first
+ offset: $offset
+ sortBy: $popularTitlesSortBy
+ filter: {
+ objectTypes: $objectTypes,
+ searchQuery: $searchQuery,
+ packages: $packages,
+ genres: [],
+ excludeGenres: [],
+ releaseYear: $releaseYear
+ }
+ ) {
+ edges {
+ node {
+ id
+ objectType
+ content(country: $country, language: $language) {
+ fullPath
+ title
+ shortDescription
+ externalIds {
+ imdbId
+ }
+ posterUrl
+ genres {
+ translation(language: $language)
+ }
+ credits {
+ name
+ role
+ }
+ }
+ }
+ }
+ pageInfo {
+ hasPreviousPage
+ hasNextPage
+ }
+ }
+ }
+ `.trim();
+ }
+ async makeGraphQLRequest(query, variables) {
+ const res = await this.client.post("https://apis.justwatch.com/graphql", { "Content-Type": "application/json" },
+ {
+ query: query,
+ variables
+ });
+ return res;
+ }
+ async searchAnimeRequest(page, query) {
+ const preferences = new SharedPreferences();
+ const country = preferences.get("jw_region");
+ const language = preferences.get("jw_lang");
+ const perPage = 40;
+ const year = 0;
+
+ const searchQueryRegex = /[^a-zA-Z0-9 ]/g;
+ const sanitizedQuery = query.replace(searchQueryRegex, "").trim();
+
+ const variables = {
+ first: perPage,
+ offset: (page - 1) * perPage,
+ platform: "WEB",
+ country: country,
+ language: language,
+ searchQuery: sanitizedQuery,
+ packages: [],
+ objectTypes: [],
+ popularTitlesSortBy: "TRENDING",
+ releaseYear: {
+ min: year,
+ max: year
+ }
+ };
+
+ return await this.makeGraphQLRequest(this.justWatchQuery(), variables);
+ }
+ parseSearchJson(jsonLine) {
+
+ const popularTitlesResponse = JSON.parse(jsonLine);
+
+ const edges = popularTitlesResponse?.data?.popularTitles?.edges || [];
+ const hasNextPage = popularTitlesResponse?.data?.popularTitles?.pageInfo?.hasNextPage || false;
+
+ const animeList = edges
+ .map(edge => {
+ const node = edge?.node;
+ const content = node?.content;
+ if (!node || !content) return null;
+ return {
+ link: `${content.externalIds?.imdbId || ""},${node.objectType || ""},${content.fullPath || ""}`,
+ name: content.title || "",
+ imageUrl: `https://images.justwatch.com${content.posterUrl?.replace("{profile}", "s276")?.replace("{format}", "webp")}`,
+ description: content.shortDescription || "",
+ genre: content.genres?.map(genre => genre.translation).filter(Boolean) || [],
+ author: (content.credits?.filter(credit => credit.role === "DIRECTOR").map(credit => credit.name) || []).join(", "),
+ artist: (content.credits?.filter(credit => credit.role === "ACTOR").slice(0, 4).map(credit => credit.name) || []).join(", "),
+ };
+ })
+ .filter(Boolean);
+
+ return { "list": animeList, hasNextPage };
+ }
+ get supportsLatest() {
+ return false;
+ }
+ async getPopular(page) {
+ return this.parseSearchJson((await this.searchAnimeRequest(page, "")).body);
+ }
+ async getLatestUpdates(page) {
+
+ }
+ async search(query, page, filters) {
+ return this.parseSearchJson((await this.searchAnimeRequest(page, query)).body);
+ }
+ async getDetail(url) {
+ const anime = {};
+ const parts = url.split(",");
+ const type = parts[1].toLowerCase();
+ const imdbId = parts[0];
+ const response = await this.client.get(`https://cinemeta-live.strem.io/meta/${type}/${imdbId}.json`);
+ const meta = JSON.parse(response.body).meta;
+ if (!meta) return anime;
+ anime.episodes = (() => {
+ switch (meta.type) {
+ case "show":
+ const videos = meta.videos || [];
+ return videos
+ .map(video => {
+ const firstAired = video.firstAired ? new Date(video.firstAired) : new Date();
+
+ return {
+ url: `/stream/series/${video.id}.json`,
+ dateUpload: firstAired.valueOf().toString(),
+ name: `S${(video.season || "").toString().trim()}:E${(video.number || "").toString()} - ${video.name || ""}`,
+ scanlator: firstAired > Date.now() ? "Upcoming" : ""
+ };
+ })
+ .sort((a, b) => {
+ const seasonA = parseInt(a.name.substringAfter("S").substringBefore(":"), 10);
+ const seasonB = parseInt(b.name.substringAfter("S").substringBefore(":"), 10);
+ const episodeA = parseInt(a.name.substringAfter("E").substringBefore(" -"), 10);
+ const episodeB = parseInt(b.name.substringAfter("E").substringBefore(" -"), 10);
+
+ return seasonA - seasonB || episodeA - episodeB;
+ })
+ .reverse();
+
+ case "movie":
+ return [
+ {
+ url: `/stream/movie/${meta.id}.json`,
+ name: "Movie"
+ }
+ ].reverse();
+
+ default:
+ return [];
+ }
+ })();
+
+ return anime;
+ }
+
+ async getVideoList(url) {
+ const preferences = new SharedPreferences();
+
+ let mainURL = `${this.source.baseUrl}/`;
+
+ const appendQueryParam = (key, values) => {
+ if (values && values.size > 0) {
+ const filteredValues = Array.from(values).filter(value => value.trim() !== "").join(",");
+ mainURL += `${key}=${filteredValues}|`;
+ }
+ };
+
+ appendQueryParam("providers", preferences.get("provider_selection"));
+ appendQueryParam("language", preferences.get("lang_selection"));
+ appendQueryParam("qualityfilter", preferences.get("quality_selection"));
+ appendQueryParam("sort", new Set([preferences.get("sorting_link")]));
+
+
+ mainURL += url;
+ mainURL = mainURL.replace(/\|$/, "");
+ const responseEpisodes = await this.client.get(mainURL);
+ const streamList = JSON.parse(responseEpisodes.body);
+
+ const animeTrackers = `
+ http://nyaa.tracker.wf:7777/announce,
+ http://anidex.moe:6969/announce,http://tracker.anirena.com:80/announce,
+ udp://tracker.uw0.xyz:6969/announce,
+ http://share.camoe.cn:8080/announce,
+ http://t.nyaatracker.com:80/announce,
+ udp://47.ip-51-68-199.eu:6969/announce,
+ udp://9.rarbg.me:2940,
+ udp://9.rarbg.to:2820,
+ udp://exodus.desync.com:6969/announce,
+ udp://explodie.org:6969/announce,
+ udp://ipv4.tracker.harry.lu:80/announce,
+ udp://open.stealth.si:80/announce,
+ udp://opentor.org:2710/announce,
+ udp://opentracker.i2p.rocks:6969/announce,
+ udp://retracker.lanta-net.ru:2710/announce,
+ udp://tracker.cyberia.is:6969/announce,
+ udp://tracker.dler.org:6969/announce,
+ udp://tracker.ds.is:6969/announce,
+ udp://tracker.internetwarriors.net:1337,
+ udp://tracker.openbittorrent.com:6969/announce,
+ udp://tracker.opentrackr.org:1337/announce,
+ udp://tracker.tiny-vps.com:6969/announce,
+ udp://tracker.torrent.eu.org:451/announce,
+ udp://valakas.rollo.dnsabr.com:2710/announce,
+ udp://www.torrent.eu.org:451/announce
+ `.split(",").map(tracker => tracker.trim()).filter(tracker => tracker);
+
+ const videos = this.sortVideos((streamList.streams || []).map(stream => {
+ const hash = `magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=${animeTrackers.join("&tr=")}&index=${stream.fileIdx}`;
+ const videoTitle = `${(stream.name || "").replace("Torrentio\n", "")}\n${stream.title || ""}`.trim();
+
+ return {
+ url: hash,
+ originalUrl: hash,
+ quality: videoTitle,
+ };
+ }));
+ const numberOfLinks = preferences.get("number_of_links");
+ if (numberOfLinks == "all") {
+ return videos;
+ }
+
+ return videos.slice(0, parseInt(numberOfLinks))
+ }
+
+ sortVideos(videos) {
+ const preferences = new SharedPreferences();
+
+ const isDub = preferences.get("dubbed");
+ const isEfficient = preferences.get("efficient");
+
+ return videos.sort((a, b) => {
+ const regexMatchA = /\[(.+?) download\]/.test(a.quality);
+ const regexMatchB = /\[(.+?) download\]/.test(b.quality);
+
+ const isDubA = isDub && !a.quality.toLowerCase().includes("dubbed");
+ const isDubB = isDub && !b.quality.toLowerCase().includes("dubbed");
+
+ const isEfficientA = isEfficient && !["hevc", "265", "av1"].some(q => a.quality.toLowerCase().includes(q));
+ const isEfficientB = isEfficient && !["hevc", "265", "av1"].some(q => b.quality.toLowerCase().includes(q));
+
+
+ return (
+ regexMatchA - regexMatchB ||
+ isDubA - isDubB ||
+ isEfficientA - isEfficientB
+ );
+ });
+ }
+
+
+
+ getSourcePreferences() {
+ return [
+ {
+ "key": "number_of_links",
+ "listPreference": {
+ "title": "Number of links to load for video list",
+ "summary": "⚠️ Increasing the number of links will increase the loading time of the video list",
+ "valueIndex": 1,
+ "entries": [
+ "2",
+ "4",
+ "8",
+ "12",
+ "all"],
+ "entryValues": [
+ "2",
+ "4",
+ "8",
+ "12",
+ "all"],
+ }
+ },
+ {
+ "key": "provider_selection",
+ "multiSelectListPreference": {
+ "title": "Enable/Disable Providers",
+ "summary": "",
+ "entries": [
+ "YTS",
+ "EZTV",
+ "RARBG",
+ "1337x",
+ "ThePirateBay",
+ "KickassTorrents",
+ "TorrentGalaxy",
+ "MagnetDL",
+ "HorribleSubs",
+ "NyaaSi",
+ "TokyoTosho",
+ "AniDex",
+ "🇷🇺 Rutor",
+ "🇷🇺 Rutracker",
+ "🇵🇹 Comando",
+ "🇵🇹 BluDV",
+ "🇫🇷 Torrent9",
+ "🇪🇸 MejorTorrent",
+ "🇲🇽 Cinecalidad"],
+ "entryValues": [
+ "yts",
+ "eztv",
+ "rarbg",
+ "1337x",
+ "thepiratebay",
+ "kickasstorrents",
+ "torrentgalaxy",
+ "magnetdl",
+ "horriblesubs",
+ "nyaasi",
+ "tokyotosho",
+ "anidex",
+ "rutor",
+ "rutracker",
+ "comando",
+ "bludv",
+ "torrent9",
+ "mejortorrent",
+ "cinecalidad"],
+ "values": [
+ "nyaasi",]
+ }
+ },
+ {
+ "key": "quality_selection",
+ "multiSelectListPreference": {
+ "title": "Exclude Qualities/Resolutions",
+ "summary": "",
+ "entries": [
+ "BluRay REMUX",
+ "HDR/HDR10+/Dolby Vision",
+ "Dolby Vision",
+ "4k",
+ "1080p",
+ "720p",
+ "480p",
+ "Other (DVDRip/HDRip/BDRip...)",
+ "Screener",
+ "Cam",
+ "Unknown"],
+ "entryValues": [
+ "brremux",
+ "hdrall",
+ "dolbyvision",
+ "4k",
+ "1080p",
+ "720p",
+ "480p",
+ "other",
+ "scr",
+ "cam",
+ "unknown"],
+ "values": [
+ "720p",
+ "480p",
+ "other",
+ "scr",
+ "cam",
+ "unknown"]
+ }
+ },
+ {
+ "key": "lang_selection",
+ "multiSelectListPreference": {
+ "title": "Priority foreign language",
+ "summary": "",
+ "entries": [
+ "🇯🇵 Japanese",
+ "🇷🇺 Russian",
+ "🇮🇹 Italian",
+ "🇵🇹 Portuguese",
+ "🇪🇸 Spanish",
+ "🇲🇽 Latino",
+ "🇰🇷 Korean",
+ "🇨🇳 Chinese",
+ "🇹🇼 Taiwanese",
+ "🇫🇷 French",
+ "🇩🇪 German",
+ "🇳🇱 Dutch",
+ "🇮🇳 Hindi",
+ "🇮🇳 Telugu",
+ "🇮🇳 Tamil",
+ "🇵🇱 Polish",
+ "🇱🇹 Lithuanian",
+ "🇱🇻 Latvian",
+ "🇪🇪 Estonian",
+ "🇨🇿 Czech",
+ "🇸🇰 Slovakian",
+ "🇸🇮 Slovenian",
+ "🇭🇺 Hungarian",
+ "🇷🇴 Romanian",
+ "🇧🇬 Bulgarian",
+ "🇷🇸 Serbian",
+ "🇭🇷 Croatian",
+ "🇺🇦 Ukrainian",
+ "🇬🇷 Greek",
+ "🇩🇰 Danish",
+ "🇫🇮 Finnish",
+ "🇸🇪 Swedish",
+ "🇳🇴 Norwegian",
+ "🇹🇷 Turkish",
+ "🇸🇦 Arabic",
+ "🇮🇷 Persian",
+ "🇮🇱 Hebrew",
+ "🇻🇳 Vietnamese",
+ "🇮🇩 Indonesian",
+ "🇲🇾 Malay",
+ "🇹🇭 Thai",],
+ "entryValues": [
+ "japanese",
+ "russian",
+ "italian",
+ "portuguese",
+ "spanish",
+ "latino",
+ "korean",
+ "chinese",
+ "taiwanese",
+ "french",
+ "german",
+ "dutch",
+ "hindi",
+ "telugu",
+ "tamil",
+ "polish",
+ "lithuanian",
+ "latvian",
+ "estonian",
+ "czech",
+ "slovakian",
+ "slovenian",
+ "hungarian",
+ "romanian",
+ "bulgarian",
+ "serbian",
+ "croatian",
+ "ukrainian",
+ "greek",
+ "danish",
+ "finnish",
+ "swedish",
+ "norwegian",
+ "turkish",
+ "arabic",
+ "persian",
+ "hebrew",
+ "vietnamese",
+ "indonesian",
+ "malay",
+ "thai"],
+ "values": []
+ }
+ },
+ {
+ "key": "sorting_link",
+ "listPreference": {
+ "title": "Sorting",
+ "summary": "",
+ "valueIndex": 0,
+ "entries": [
+ "By quality then seeders",
+ "By quality then size",
+ "By seeders",
+ "By size"],
+ "entryValues": [
+ "quality",
+ "qualitysize",
+ "seeders",
+ "size"],
+ }
+ },
+ {
+ "key": "dubbed",
+ "switchPreferenceCompat": {
+ "title": "Dubbed Video Priority",
+ "summary": "",
+ "value": false
+ }
+ },
+ {
+ "key": "efficient",
+ "switchPreferenceCompat": {
+ "title": "Efficient Video Priority",
+ "summary": "Codec: (HEVC / x265) & AV1. High-quality video with less data usage.",
+ "value": false
+ }
+ },
+ {
+ "key": "jw_region",
+ "listPreference": {
+ "title": "Catalogue Region",
+ "summary": "Region based catalogue recommendation.",
+ "valueIndex": 133,
+ "entries": [
+ "Albania", "Algeria", "Androrra", "Angola", "Antigua and Barbuda", "Argentina", "Australia", "Austria", "Azerbaijan", "Bahamas", "Bahrain", "Barbados", "Belarus", "Belgium", "Belize", "Bermuda", "Bolivia", "Bosnia and Herzegovina", "Brazil", "Bulgaria", "Burkina Faso", "Cameroon", "Canada", "Cape Verde", "Chad", "Chile", "Colombia", "Costa Rica", "Croatia", "Cuba", "Cyprus", "Czech Republic", "DR Congo", "Denmark", "Dominican Republic", "Ecuador", "Egypt", "El Salvador", "Equatorial Guinea", "Estonia", "Fiji", "Finland", "France", "French Guiana", "French Polynesia", "Germany", "Ghana", "Gibraltar", "Greece", "Guatemala", "Guernsey", "Guyana", "Honduras", "Hong Kong", "Hungary", "Iceland", "India", "Indonesia", "Iraq", "Ireland", "Israel", "Italy", "Ivory Coast", "Jamaica", "Japan", "Jordan", "Kenya", "Kosovo", "Kuwait", "Latvia", "Lebanon", "Libya", "Liechtenstein", "Lithuania", "Luxembourg", "Macedonia", "Madagascar", "Malawi", "Malaysia", "Mali", "Malta", "Mauritius", "Mexico", "Moldova", "Monaco", "Montenegro", "Morocco", "Mozambique", "Netherlands", "New Zealand", "Nicaragua", "Niger", "Nigeria", "Norway", "Oman", "Pakistan", "Palestine", "Panama", "Papua New Guinea", "Paraguay", "Peru", "Philippines", "Poland", "Portugal", "Qatar", "Romania", "Russia", "Saint Lucia", "San Marino", "Saudi Arabia", "Senegal", "Serbia", "Seychelles", "Singapore", "Slovakia", "Slovenia", "South Africa", "South Korea", "Spain", "Sweden", "Switzerland", "Taiwan", "Tanzania", "Thailand", "Trinidad and Tobago", "Tunisia", "Turkey", "Turks and Caicos Islands", "Uganda", "Ukraine", "United Arab Emirates", "United Kingdom", "United States", "Uruguay", "Vatican City", "Venezuela", "Yemen", "Zambia", "Zimbabwe"],
+ "entryValues": [
+ "AL", "DZ", "AD", "AO", "AG", "AR", "AU", "AT", "AZ", "BS", "BH", "BB", "BY", "BE", "BZ", "BM", "BO", "BA", "BR", "BG", "BF", "CM", "CA", "CV", "TD", "CL", "CO", "CR", "HR", "CU", "CY", "CZ", "CD", "DK", "DO", "EC", "EG", "SV", "GQ", "EE", "FJ", "FI", "FR", "GF", "PF", "DE", "GH", "GI", "GR", "GT", "GG", "GY", "HN", "HK", "HU", "IS", "IN", "ID", "IQ", "IE", "IL", "IT", "CI", "JM", "JP", "JO", "KE", "XK", "KW", "LV", "LB", "LY", "LI", "LT", "LU", "MK", "MG", "MW", "MY", "ML", "MT", "MU", "MX", "MD", "MC", "ME", "MA", "MZ", "NL", "NZ", "NI", "NE", "NG", "NO", "OM", "PK", "PS", "PA", "PG", "PY", "PE", "PH", "PL", "PT", "QA", "RO", "RU", "LC", "SM", "SA", "SN", "RS", "SC", "SG", "SK", "SI", "ZA", "KR", "ES", "SE", "CH", "TW", "TZ", "TH", "TT", "TN", "TR", "TC", "UG", "UA", "AE", "UK", "US", "UY", "VA", "VE", "YE", "ZM", "ZW"],
+ }
+ },
+ {
+ "key": "jw_lang",
+ "listPreference": {
+ "title": "Poster and Titles Language",
+ "summary": "",
+ "valueIndex": 9,
+ "entries": [
+ "Arabic",
+ "Azerbaijani",
+ "Belarusian",
+ "Bulgarian",
+ "Bosnian",
+ "Catalan",
+ "Czech",
+ "German",
+ "Greek",
+ "English",
+ "English (U.S.A.)",
+ "Spanish",
+ "Spanish (Spain)",
+ "Spanish (Latinamerican)",
+ "Estonian",
+ "Finnish",
+ "French",
+ "French (Canada)",
+ "Hebrew",
+ "Croatian",
+ "Hungarian",
+ "Icelandic",
+ "Italian",
+ "Japanese",
+ "Korean",
+ "Lithuanian",
+ "Latvian",
+ "Macedonian",
+ "Maltese",
+ "Polish",
+ "Portuguese",
+ "Portuguese (Portugal)",
+ "Portuguese (Brazil)",
+ "Romanian",
+ "Russian",
+ "Slovakian",
+ "Slovenian",
+ "Albanian",
+ "Serbian",
+ "Swedish",
+ "Swahili",
+ "Turkish",
+ "Ukrainian",
+ "Urdu",
+ "Chinese"],
+ "entryValues": [
+ "ar",
+ "az",
+ "be",
+ "bg",
+ "bs",
+ "ca",
+ "cs",
+ "de",
+ "el",
+ "en",
+ "en-US",
+ "es",
+ "es-ES",
+ "es-LA",
+ "et",
+ "fi",
+ "fr",
+ "fr-CA",
+ "he",
+ "hr",
+ "hu",
+ "is",
+ "it",
+ "ja",
+ "ko",
+ "lt",
+ "lv",
+ "mk",
+ "mt",
+ "pl",
+ "pt",
+ "pt-PT",
+ "pt-BR",
+ "ro",
+ "ru",
+ "sk",
+ "sl",
+ "sq",
+ "sr",
+ "sv",
+ "sw",
+ "tr",
+ "uk",
+ "ur",
+ "zh"],
+ }
+ },
+ ];
+ }
+}
diff --git a/javascript/anime/src/all/torrentioanime.js b/javascript/anime/src/all/torrentioanime.js
new file mode 100644
index 00000000..f62d948e
--- /dev/null
+++ b/javascript/anime/src/all/torrentioanime.js
@@ -0,0 +1,636 @@
+const mangayomiSources = [{
+ "name": "Torrentio Anime (Torrent)",
+ "lang": "all",
+ "baseUrl": "https://torrentio.strem.fun",
+ "apiUrl": "",
+ "iconUrl": "https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/main/javascript/icon/all.torrentio.png",
+ "typeSource": "torrent",
+ "isManga": false,
+ "version": "0.0.1",
+ "appMinVerReq": "0.3.75",
+ "pkgPath": "anime/src/all/torrentioanime.js"
+}];
+
+class DefaultExtension extends MProvider {
+ constructor() {
+ super();
+ this.client = new Client();
+ }
+
+ anilistQuery() {
+ return `
+ query ($page: Int, $perPage: Int, $sort: [MediaSort], $search: String) {
+ Page(page: $page, perPage: $perPage) {
+ pageInfo {
+ currentPage
+ hasNextPage
+ }
+ media(type: ANIME, sort: $sort, search: $search, status_in: [RELEASING, FINISHED, NOT_YET_RELEASED]) {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags {
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ }
+ `.trim();
+ }
+ anilistLatestQuery() {
+ const currentTimeInSeconds = Math.floor(Date.now() / 1000);
+ return `
+ query ($page: Int, $perPage: Int, $sort: [AiringSort]) {
+ Page(page: $page, perPage: $perPage) {
+ pageInfo {
+ currentPage
+ hasNextPage
+ }
+ airingSchedules(
+ airingAt_greater: 0
+ airingAt_lesser: ${currentTimeInSeconds - 10000}
+ sort: $sort
+ ) {
+ media {
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags {
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ }
+ }
+ `.trim();
+ }
+ async makeGraphQLRequest(query, variables) {
+ const res = await this.client.post("https://graphql.anilist.co", {},
+ {
+ query, variables
+ });
+ return res;
+ }
+ parseSearchJson(jsonLine, isLatestQuery = false) {
+ const jsonData = JSON.parse(jsonLine);
+ jsonData.type = isLatestQuery ? "AnilistMetaLatest" : "AnilistMeta";
+ const metaData = jsonData;
+
+ const mediaList = metaData.type == "AnilistMeta"
+ ? metaData.data?.Page?.media || []
+ : metaData.data?.Page?.airingSchedules.map(schedule => schedule.media) || [];
+
+ const hasNextPage = metaData.type == "AnilistMeta" || metaData.type == "AnilistMetaLatest"
+ ? metaData.data?.Page?.pageInfo?.hasNextPage || false
+ : false;
+
+ const animeList = mediaList
+ .filter(media => !((media?.countryOfOrigin === "CN" || media?.isAdult) && isLatestQuery))
+ .map(media => {
+ const anime = {};
+ anime.link = media?.id?.toString() || "";
+ anime.name = (() => {
+ const preferenceTitle = new SharedPreferences().get("pref_title")
+ switch (preferenceTitle) {
+ case "romaji":
+ return media?.title?.romaji || "";
+ case "english":
+ return media?.title?.english?.trim() || media?.title?.romaji || "";
+ case "native":
+ return media?.title?.native || "";
+ default:
+ return "";
+ }
+ })();
+ anime.imageUrl = media?.coverImage?.extraLarge || "";
+
+ return anime;
+ });
+
+ return { "list": animeList, "hasNextPage": hasNextPage };
+ }
+ async getPopular(page) {
+ const variables = JSON.stringify({
+ page: page,
+ perPage: 30,
+ sort: "TRENDING_DESC"
+ });
+
+ const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
+
+ return this.parseSearchJson(res.body)
+ }
+ async getLatestUpdates(page) {
+ const variables = JSON.stringify({
+ page: page,
+ perPage: 30,
+ sort: "TIME_DESC"
+ });
+
+ const res = await this.makeGraphQLRequest(this.anilistLatestQuery(), variables);
+
+ return this.parseSearchJson(res.body, true)
+ }
+ async search(query, page, filters) {
+ const variables = JSON.stringify({
+ page: page,
+ perPage: 30,
+ sort: "POPULARITY_DESC",
+ search: query
+ });
+
+ const res = await this.makeGraphQLRequest(this.anilistQuery(), variables);
+
+ return this.parseSearchJson(res.body)
+ }
+ async getDetail(url) {
+ const query = `
+ query($id: Int){
+ Media(id: $id){
+ id
+ title {
+ romaji
+ english
+ native
+ }
+ coverImage {
+ extraLarge
+ large
+ }
+ description
+ status
+ tags {
+ name
+ }
+ genres
+ studios {
+ nodes {
+ name
+ }
+ }
+ countryOfOrigin
+ isAdult
+ }
+ }
+ `.trim();
+
+ const variables = JSON.stringify({ id: url });
+
+ const res = await this.makeGraphQLRequest(query, variables);
+ const media = JSON.parse(res.body).data.Media;
+ const anime = {};
+ anime.imageUrl = media?.coverImage?.extraLarge || "";
+ anime.description = (media?.description || "No Description")
+ .replace(/
/g, "\n")
+ .replace(/<.*?>/g, "");
+
+ anime.status = (() => {
+ switch (media?.status) {
+ case "RELEASING":
+ return 0;
+ case "FINISHED":
+ return 1;
+ case "HIATUS":
+ return 2;
+ case "NOT_YET_RELEASED":
+ return 3;
+ default:
+ return 5;
+ }
+ })();
+
+ const tagsList = media?.tags?.map(tag => tag.name).filter(Boolean) || [];
+ const genresList = media?.genres || [];
+ anime.genre = [...new Set([...tagsList, ...genresList])].sort();
+ const studiosList = media?.studios?.nodes?.map(node => node.name).filter(Boolean) || [];
+ anime.author = studiosList.sort().join(", ");
+ const response = await this.client.get(`https://api.ani.zip/mappings?anilist_id=${url}`);
+ const kitsuId = JSON.parse(response.body).mappings.kitsu_id.toString();
+ const responseEpisodes = await this.client.get(`https://anime-kitsu.strem.fun/meta/series/kitsu%3A${kitsuId}.json`);
+ const episodeList = JSON.parse(responseEpisodes.body);
+ anime.episodes = (() => {
+ switch (episodeList.meta?.type) {
+ case "series": {
+ const videos = episodeList.meta.videos;
+ return videos
+ .filter(video => video.thumbnail !== null)
+ .map(video => {
+ const releaseDate = video.released ? new Date(video.released) : Date.now();
+ const upcoming = releaseDate > Date.now() ? "Upcoming" : "";
+ return {
+ url: `/stream/series/${video.id}.json`,
+ dateUpload: releaseDate.valueOf().toString(),
+ name: `Episode ${video.episode} : ${video.title
+ ?.replace(/^Episode /, "")
+ ?.replace(/^\d+\s*/, "")
+ ?.trim()}`,
+ scanlator: upcoming,
+ };
+ })
+ .reverse();
+ }
+
+ case "movie": {
+ const kitsuId = episodeList.meta.kitsuId;
+
+ return [
+ {
+ url: `/stream/movie/${kitsuId}.json`,
+ name: "Movie",
+ },
+ ].reverse();
+ }
+
+ default:
+ return [];
+ }
+ })()
+
+
+ return anime;
+ }
+
+ async getVideoList(url) {
+ const preferences = new SharedPreferences();
+
+ let mainURL = `${this.source.baseUrl}/`;
+
+ const appendQueryParam = (key, values) => {
+ if (values && values.size > 0) {
+ const filteredValues = Array.from(values).filter(value => value.trim() !== "").join(",");
+ mainURL += `${key}=${filteredValues}|`;
+ }
+ };
+
+ appendQueryParam("providers", preferences.get("provider_selection"));
+ appendQueryParam("language", preferences.get("lang_selection"));
+ appendQueryParam("qualityfilter", preferences.get("quality_selection"));
+ appendQueryParam("sort", new Set([preferences.get("sorting_link")]));
+
+
+ mainURL += url;
+ mainURL = mainURL.replace(/\|$/, "");
+ const responseEpisodes = await this.client.get(mainURL);
+ const streamList = JSON.parse(responseEpisodes.body);
+
+ const animeTrackers = `
+ http://nyaa.tracker.wf:7777/announce,
+ http://anidex.moe:6969/announce,http://tracker.anirena.com:80/announce,
+ udp://tracker.uw0.xyz:6969/announce,
+ http://share.camoe.cn:8080/announce,
+ http://t.nyaatracker.com:80/announce,
+ udp://47.ip-51-68-199.eu:6969/announce,
+ udp://9.rarbg.me:2940,
+ udp://9.rarbg.to:2820,
+ udp://exodus.desync.com:6969/announce,
+ udp://explodie.org:6969/announce,
+ udp://ipv4.tracker.harry.lu:80/announce,
+ udp://open.stealth.si:80/announce,
+ udp://opentor.org:2710/announce,
+ udp://opentracker.i2p.rocks:6969/announce,
+ udp://retracker.lanta-net.ru:2710/announce,
+ udp://tracker.cyberia.is:6969/announce,
+ udp://tracker.dler.org:6969/announce,
+ udp://tracker.ds.is:6969/announce,
+ udp://tracker.internetwarriors.net:1337,
+ udp://tracker.openbittorrent.com:6969/announce,
+ udp://tracker.opentrackr.org:1337/announce,
+ udp://tracker.tiny-vps.com:6969/announce,
+ udp://tracker.torrent.eu.org:451/announce,
+ udp://valakas.rollo.dnsabr.com:2710/announce,
+ udp://www.torrent.eu.org:451/announce
+ `.split(",").map(tracker => tracker.trim()).filter(tracker => tracker);
+
+ const videos = this.sortVideos((streamList.streams || []).map(stream => {
+ const hash = `magnet:?xt=urn:btih:${stream.infoHash}&dn=${stream.infoHash}&tr=${animeTrackers.join("&tr=")}&index=${stream.fileIdx}`;
+ const videoTitle = `${(stream.name || "").replace("Torrentio\n", "")}\n${stream.title || ""}`.trim();
+
+ return {
+ url: hash,
+ originalUrl: hash,
+ quality: videoTitle,
+ };
+ }));
+ const numberOfLinks = preferences.get("number_of_links");
+ if (numberOfLinks == "all") {
+ return videos;
+ }
+
+ return videos.slice(0, parseInt(numberOfLinks))
+ }
+
+ sortVideos(videos) {
+ const preferences = new SharedPreferences();
+
+ const isDub = preferences.get("dubbed");
+ const isEfficient = preferences.get("efficient");
+
+ return videos.sort((a, b) => {
+ const regexMatchA = /\[(.+?) download\]/.test(a.quality);
+ const regexMatchB = /\[(.+?) download\]/.test(b.quality);
+
+ const isDubA = isDub && !a.quality.toLowerCase().includes("dubbed");
+ const isDubB = isDub && !b.quality.toLowerCase().includes("dubbed");
+
+ const isEfficientA = isEfficient && !["hevc", "265", "av1"].some(q => a.quality.toLowerCase().includes(q));
+ const isEfficientB = isEfficient && !["hevc", "265", "av1"].some(q => b.quality.toLowerCase().includes(q));
+
+
+ return (
+ regexMatchA - regexMatchB ||
+ isDubA - isDubB ||
+ isEfficientA - isEfficientB
+ );
+ });
+ }
+
+
+
+ getSourcePreferences() {
+ return [
+ {
+ "key": "number_of_links",
+ "listPreference": {
+ "title": "Number of links to load for video list",
+ "summary": "⚠️ Increasing the number of links will increase the loading time of the video list",
+ "valueIndex": 1,
+ "entries": [
+ "2",
+ "4",
+ "8",
+ "12",
+ "all"],
+ "entryValues": [
+ "2",
+ "4",
+ "8",
+ "12",
+ "all"],
+ }
+ },
+ {
+ "key": "provider_selection",
+ "multiSelectListPreference": {
+ "title": "Enable/Disable Providers",
+ "summary": "",
+ "entries": [
+ "YTS",
+ "EZTV",
+ "RARBG",
+ "1337x",
+ "ThePirateBay",
+ "KickassTorrents",
+ "TorrentGalaxy",
+ "MagnetDL",
+ "HorribleSubs",
+ "NyaaSi",
+ "TokyoTosho",
+ "AniDex",
+ "🇷🇺 Rutor",
+ "🇷🇺 Rutracker",
+ "🇵🇹 Comando",
+ "🇵🇹 BluDV",
+ "🇫🇷 Torrent9",
+ "🇪🇸 MejorTorrent",
+ "🇲🇽 Cinecalidad"],
+ "entryValues": [
+ "yts",
+ "eztv",
+ "rarbg",
+ "1337x",
+ "thepiratebay",
+ "kickasstorrents",
+ "torrentgalaxy",
+ "magnetdl",
+ "horriblesubs",
+ "nyaasi",
+ "tokyotosho",
+ "anidex",
+ "rutor",
+ "rutracker",
+ "comando",
+ "bludv",
+ "torrent9",
+ "mejortorrent",
+ "cinecalidad"],
+ "values": [
+ "nyaasi",]
+ }
+ },
+ {
+ "key": "quality_selection",
+ "multiSelectListPreference": {
+ "title": "Exclude Qualities/Resolutions",
+ "summary": "",
+ "entries": [
+ "BluRay REMUX",
+ "HDR/HDR10+/Dolby Vision",
+ "Dolby Vision",
+ "4k",
+ "1080p",
+ "720p",
+ "480p",
+ "Other (DVDRip/HDRip/BDRip...)",
+ "Screener",
+ "Cam",
+ "Unknown"],
+ "entryValues": [
+ "brremux",
+ "hdrall",
+ "dolbyvision",
+ "4k",
+ "1080p",
+ "720p",
+ "480p",
+ "other",
+ "scr",
+ "cam",
+ "unknown"],
+ "values": [
+ "720p",
+ "480p",
+ "other",
+ "scr",
+ "cam",
+ "unknown"]
+ }
+ },
+ {
+ "key": "lang_selection",
+ "multiSelectListPreference": {
+ "title": "Priority foreign language",
+ "summary": "",
+ "entries": [
+ "🇯🇵 Japanese",
+ "🇷🇺 Russian",
+ "🇮🇹 Italian",
+ "🇵🇹 Portuguese",
+ "🇪🇸 Spanish",
+ "🇲🇽 Latino",
+ "🇰🇷 Korean",
+ "🇨🇳 Chinese",
+ "🇹🇼 Taiwanese",
+ "🇫🇷 French",
+ "🇩🇪 German",
+ "🇳🇱 Dutch",
+ "🇮🇳 Hindi",
+ "🇮🇳 Telugu",
+ "🇮🇳 Tamil",
+ "🇵🇱 Polish",
+ "🇱🇹 Lithuanian",
+ "🇱🇻 Latvian",
+ "🇪🇪 Estonian",
+ "🇨🇿 Czech",
+ "🇸🇰 Slovakian",
+ "🇸🇮 Slovenian",
+ "🇭🇺 Hungarian",
+ "🇷🇴 Romanian",
+ "🇧🇬 Bulgarian",
+ "🇷🇸 Serbian",
+ "🇭🇷 Croatian",
+ "🇺🇦 Ukrainian",
+ "🇬🇷 Greek",
+ "🇩🇰 Danish",
+ "🇫🇮 Finnish",
+ "🇸🇪 Swedish",
+ "🇳🇴 Norwegian",
+ "🇹🇷 Turkish",
+ "🇸🇦 Arabic",
+ "🇮🇷 Persian",
+ "🇮🇱 Hebrew",
+ "🇻🇳 Vietnamese",
+ "🇮🇩 Indonesian",
+ "🇲🇾 Malay",
+ "🇹🇭 Thai",],
+ "entryValues": [
+ "japanese",
+ "russian",
+ "italian",
+ "portuguese",
+ "spanish",
+ "latino",
+ "korean",
+ "chinese",
+ "taiwanese",
+ "french",
+ "german",
+ "dutch",
+ "hindi",
+ "telugu",
+ "tamil",
+ "polish",
+ "lithuanian",
+ "latvian",
+ "estonian",
+ "czech",
+ "slovakian",
+ "slovenian",
+ "hungarian",
+ "romanian",
+ "bulgarian",
+ "serbian",
+ "croatian",
+ "ukrainian",
+ "greek",
+ "danish",
+ "finnish",
+ "swedish",
+ "norwegian",
+ "turkish",
+ "arabic",
+ "persian",
+ "hebrew",
+ "vietnamese",
+ "indonesian",
+ "malay",
+ "thai"],
+ "values": []
+ }
+ },
+ {
+ "key": "sorting_link",
+ "listPreference": {
+ "title": "Sorting",
+ "summary": "",
+ "valueIndex": 0,
+ "entries": [
+ "By quality then seeders",
+ "By quality then size",
+ "By seeders",
+ "By size"],
+ "entryValues": [
+ "quality",
+ "qualitysize",
+ "seeders",
+ "size"],
+ }
+ },
+ {
+ "key": "pref_title",
+ "listPreference": {
+ "title": "Preferred Title",
+ "summary": "",
+ "valueIndex": 0,
+ "entries": [
+ "Romaji",
+ "English",
+ "Native"],
+ "entryValues": [
+ "romaji",
+ "english",
+ "native"],
+ }
+ },
+ {
+ "key": "dubbed",
+ "switchPreferenceCompat": {
+ "title": "Dubbed Video Priority",
+ "summary": "",
+ "value": false
+ }
+ },
+ {
+ "key": "efficient",
+ "switchPreferenceCompat": {
+ "title": "Efficient Video Priority",
+ "summary": "Codec: (HEVC / x265) & AV1. High-quality video with less data usage.",
+ "value": false
+ }
+ }
+ ];
+ }
+}
diff --git a/javascript/icon/all.torrentio.png b/javascript/icon/all.torrentio.png
new file mode 100644
index 00000000..99699d40
Binary files /dev/null and b/javascript/icon/all.torrentio.png differ
diff --git a/source_generator.dart b/source_generator.dart
index e6a6e694..25051b2a 100644
--- a/source_generator.dart
+++ b/source_generator.dart
@@ -58,7 +58,8 @@ List _searchJsSources(Directory dir) {
final langs = sourceJson["langs"] as List?;
Source source = Source.fromJson(sourceJson)
..sourceCodeLanguage = 1
- ..appMinVerReq = defaultSource.appMinVerReq
+ ..appMinVerReq =
+ sourceJson["appMinVerReq"] ?? defaultSource.appMinVerReq
..sourceCodeUrl =
"https://raw.githubusercontent.com/kodjodevf/mangayomi-extensions/$branchName/javascript/${sourceJson["pkgPath"] ?? sourceJson["pkgName"]}";
if (sourceJson["id"] != null) {