diff --git a/anineko/anineko.js b/anineko/anineko.js new file mode 100644 index 0000000..db4b25e --- /dev/null +++ b/anineko/anineko.js @@ -0,0 +1,316 @@ +async function searchResults(keyword) { + const results = []; + try { + const response = await fetchv2("https://anineko.to/browser?keyword=" + encodeURIComponent(keyword)); + const html = await response.text(); + + const regex = /
[\s\S]*?]+href="([^"]+)"[^>]*>[\s\S]*?]+src="([^"]+)"[^>]+alt="([^"]+)"/g; + let match; + while ((match = regex.exec(html)) !== null) { + results.push({ + title: match[3].trim(), + image: match[2].trim(), + href: "https://anineko.to" + match[1].trim() + }); + } + + return JSON.stringify(results); + } catch (err) { + return JSON.stringify([{ + title: "Error", + image: "Error", + href: "Error" + }]); + } +} + +async function extractDetails(url) { + try { + const response = await fetchv2(url); + const html = await response.text(); + + let description = "N/A"; + const descMatch = html.match(/[\s\S]*?]+href="([^"]+)"[^>]*>[\s\S]*?Episode (\d+)<\/strong>/g; + let match; + while ((match = regex.exec(html)) !== null) { + results.push({ + href: "https://anineko.to" + match[1].trim(), + number: parseInt(match[2], 10) + }); + } + + return JSON.stringify(results); + } catch (err) { + return JSON.stringify([{ + href: "Error", + number: "Error" + }]); + } +} + +async function extractStreamUrl(url) { + try { + const response = await fetchv2(url); + const html = await response.text(); + + const serverTasks = []; + let subtitles = ""; + + const regex = /]+data-video="([^"]+)"[^>]*>\s*([^<\s]+)\s*([^<]+)<\/span>/g; + let match; + while ((match = regex.exec(html)) !== null) { + const videoUrl = match[1]; + const serverName = match[2].trim(); + let label = match[3].trim(); + + if (label === "Sort Sub") label = "Soft Sub"; + + if (!subtitles) { + const subMatch = videoUrl.match(/(?:sub|caption_1|c1_file)=([^&"]+)/); + if (subMatch) { + subtitles = decodeURIComponent(subMatch[1]); + } + } + + serverTasks.push((async () => { + let streamUrl = null; + let priority = 99; + + try { + if (serverName === "HD-1" || serverName === "HD-2") { + priority = serverName === "HD-1" ? 1 : 2; + if (videoUrl.includes("vibeplayer.site")) { + const idMatch = videoUrl.match(/vibeplayer\.site\/([a-z0-9]+)/); + if (idMatch) { + streamUrl = `https://vibeplayer.site/public/stream/${idMatch[1]}/master.m3u8`; + } + } + } else if (serverName === "StreamHG" || serverName === "Earnvids") { + priority = serverName === "StreamHG" ? 3 : 4; + const playerResponse = await fetchv2(videoUrl); + const playerHtml = await playerResponse.text(); + const obfuscatedScript = playerHtml.match(/]*>\s*(eval\(function\(p,a,c,k,e,d.*?\)[\s\S]*?)<\/script>/); + if (obfuscatedScript) { + const unpackedScript = unpack(obfuscatedScript[1]); + + const hlsMatch = unpackedScript.match(/"(https:\/\/[^"]+master\.m3u8[^"]*)"/); + if (hlsMatch) { + streamUrl = hlsMatch[1]; + } else { + const fileMatch = unpackedScript.match(/file\s*:\s*"([^"]+)"/); + if (fileMatch) streamUrl = fileMatch[1]; + } + } + } else if (serverName === "Doodstream") { + priority = 5; + const playerResponse = await fetchv2(videoUrl); + const playerHtml = await playerResponse.text(); + streamUrl = await doodstreamExtractor(playerHtml, videoUrl); + } + } catch (e) { + console.log("Error extracting server " + serverName + ": " + e); + } + + if (streamUrl) { + return { serverName, label, priority, streamUrl }; + } + return null; + })()); + } + + const resolvedResults = await Promise.all(serverTasks); + const validStreams = resolvedResults.filter(s => s !== null); + + validStreams.sort((a, b) => a.priority - b.priority); + + const streams = []; + const serverCounts = {}; + + for (const s of validStreams) { + let baseName = s.serverName.replace("-", " "); + let baseTitle = ""; + if (s.serverName === "HD-1" || s.serverName === "HD-2") { + baseTitle = `[👑] ${baseName} ${s.label}`; + } else { + baseTitle = `${baseName} ${s.label}`; + } + + let finalTitle = baseTitle; + if (serverCounts[baseTitle]) { + serverCounts[baseTitle]++; + finalTitle = `${baseTitle} ${serverCounts[baseTitle]}`; + } else { + serverCounts[baseTitle] = 1; + } + + streams.push({ + title: finalTitle, + streamUrl: s.streamUrl, + headers: {} + }); + } + + return JSON.stringify({ + streams: streams, + subtitles: subtitles + }); + } catch (err) { + return JSON.stringify({ + streams: [], + subtitles: "" + }); + } +} + +async function doodstreamExtractor(html, url) { + try { + const streamDomain = url.match(/https:\/\/(.*?)\//)[1]; + const md5Match = html.match(/'\/pass_md5\/(.*?)',/); + if (!md5Match) return null; + const md5Path = md5Match[1]; + + const token = md5Path.substring(md5Path.lastIndexOf("/") + 1); + const expiryTimestamp = new Date().valueOf(); + const random = randomStr(10); + + const passResponse = await fetchv2(`https://${streamDomain}/pass_md5/${md5Path}`, { + headers: { + "Referer": url, + }, + }); + const responseData = await passResponse.text(); + return `${responseData}${random}?token=${token}&expiry=${expiryTimestamp}`; + } catch (e) { + return null; + } +} + +function randomStr(length) { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + let result = ""; + for (let i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; +} + +class Unbaser { + constructor(base) { + this.ALPHABET = { + 62: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ", + 95: "' !\"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~'", + }; + this.dictionary = {}; + this.base = base; + if (36 < base && base < 62) { + this.ALPHABET[base] = this.ALPHABET[base] || + this.ALPHABET[62].substr(0, base); + } + if (2 <= base && base <= 36) { + this.unbase = (value) => parseInt(value, base); + } + else { + try { + [...this.ALPHABET[base]].forEach((cipher, index) => { + this.dictionary[cipher] = index; + }); + } + catch (er) { + throw Error("Unsupported base encoding."); + } + this.unbase = this._dictunbaser; + } + } + _dictunbaser(value) { + let ret = 0; + [...value].reverse().forEach((cipher, index) => { + ret = ret + ((Math.pow(this.base, index)) * this.dictionary[cipher]); + }); + return ret; + } +} + +function detect(source) { + return source.replace(" ", "").startsWith("eval(function(p,a,c,k,e,"); +} + +function unpack(source) { + let { payload, symtab, radix, count } = _filterargs(source); + if (count != symtab.length) { + throw Error("Malformed p.a.c.k.e.r. symtab."); + } + let unbase; + try { + unbase = new Unbaser(radix); + } + catch (e) { + throw Error("Unknown p.a.c.k.e.r. encoding."); + } + function lookup(match) { + const word = match; + let word2; + if (radix == 1) { + word2 = symtab[parseInt(word)]; + } + else { + word2 = symtab[unbase.unbase(word)]; + } + return word2 || word; + } + source = payload.replace(/\b\w+\b/g, lookup); + return _replacestrings(source); + function _filterargs(source) { + const juicers = [ + /}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\), *(\d+), *(.*)\)\)/, + /}\('(.*)', *(\d+|\[\]), *(\d+), *'(.*)'\.split\('\|'\)/, + ]; + for (const juicer of juicers) { + const args = juicer.exec(source); + if (args) { + let a = args; + if (a[2] == "[]") { + } + try { + return { + payload: a[1], + symtab: a[4].split("|"), + radix: parseInt(a[2]), + count: parseInt(a[3]), + }; + } + catch (ValueError) { + throw Error("Corrupted p.a.c.k.e.r. data."); + } + } + } + throw Error("Could not make sense of p.a.c.k.e.r data (unexpected code structure)"); + } + function _replacestrings(source) { + return source; + } +} diff --git a/anineko/anineko.json b/anineko/anineko.json new file mode 100644 index 0000000..228c52d --- /dev/null +++ b/anineko/anineko.json @@ -0,0 +1,27 @@ +{ + "sourceName": "AniNeko", + "iconUrl": "https://files.catbox.moe/6tgiww.png", + "author": { + "name": "50/50", + "icon": "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ3122kQwublLkZ6rf1fEpUP79BxZOFmH9BSA&s" + }, + "version": "1.0.0", + "language": "English", + "streamType": "HLS", + "quality": "1080p", + "baseUrl": "https://anineko.to/", + "searchBaseUrl": "https://anineko.to/", + "scriptUrl": "https://git.luna-app.eu/50n50/sources/raw/branch/main/anineko/anineko.js", + "type": "anime", + "asyncJS": true, + "softsub": true, + "downloadSupport": true, + "supportsMojuru": true, + "supportsDartotsu": true, + "supportsSora": true, + "supportsLuna": true, + "supportsAnymex": true, + "supportsTsumi": true, + "supportsHiyoku": true, + "supportsShirox": true +} \ No newline at end of file diff --git a/anistream/anistream.js b/anistream/anistream.js new file mode 100644 index 0000000..6fd6753 --- /dev/null +++ b/anistream/anistream.js @@ -0,0 +1,302 @@ +const GRAPHQL_URL = "https://graphql.animex.one/graphql"; +const REST_URL = "https://pp.animex.one/rest/api"; +const HEADERS = { + "Content-Type": "application/json", + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +}; + +// ─── Helper: Custom fetch with fetchv2 fallback ─── +async function soraFetch(url, options = {}) { + try { + const method = options.method || "GET"; + const headers = options.headers || {}; + const body = options.body || null; + const res = await fetchv2(url, headers, method, body); + return res; + } catch (e) { + try { + const res = await fetch(url, { + method: options.method || "GET", + headers: options.headers || {}, + body: options.body || undefined + }); + return res; + } catch (err) { + return null; + } + } +} + +// ─── Helper: GraphQL query ─── +async function graphqlQuery(query) { + const res = await soraFetch(GRAPHQL_URL, { + method: "POST", + headers: HEADERS, + body: JSON.stringify({ query: query, variables: {} }) + }); + if (!res) return null; + try { + const data = await res.json(); + return data.data; + } catch (e) { + return null; + } +} + +// ─── Helper: REST API GET ─── +async function restGet(endpoint) { + const res = await soraFetch(REST_URL + endpoint, { headers: HEADERS }); + if (!res) return null; + try { + return await res.json(); + } catch (e) { + return null; + } +} + +// ─── Helper: Ensure Referer ends with / ─── +function fixReferer(referer) { + if (!referer) return ""; + if (referer.endsWith("/") || referer.endsWith('"')) return referer; + return referer + "/"; +} + +// ─── 1. Search Results ─── +async function searchResults(keyword) { + try { + const query = `{ searchAnime(query: "${keyword.replace(/"/g, '\\"')}", limit: 24) { items { anilistId titleRomaji titleEnglish coverImage } } }`; + const data = await graphqlQuery(query); + + const searchResult = data.searchAnime; + if (!searchResult || !searchResult.items || !Array.isArray(searchResult.items)) { + return JSON.stringify([{ title: "No results found", image: "Error", href: "Error" }]); + } + + const results = []; + for (const anime of searchResult.items) { + const title = anime.titleEnglish || anime.titleRomaji || "Unknown"; + const cover = anime.coverImage; + const image = (cover && cover.extraLarge) ? cover.extraLarge : ((cover && cover.large) ? cover.large : "Error"); + const anilistId = anime.anilistId || anime.id; + results.push({ + title: title, + image: image, + href: String(anilistId) + }); + } + + if (results.length === 0) { + return JSON.stringify([{ title: "No results found", image: "Error", href: "Error" }]); + } + + return JSON.stringify(results); + } catch (err) { + return JSON.stringify([{ title: "Error", image: "Error", href: "Error" }]); + } +} + +// ─── 2. Extract Details ─── +async function extractDetails(url) { + try { + const anilistId = url.trim(); + const query = `{ anime(anilistId: ${anilistId}) { titleRomaji titleEnglish description status format episodeCount seasonYear season averageScore genres synonyms } }`; + const data = await graphqlQuery(query); + + if (!data || !data.anime) { + return JSON.stringify([{ description: "Error", aliases: "Error", airdate: "Error" }]); + } + + const anime = data.anime; + const desc = anime.description || "No description available."; + const aliases = (anime.synonyms && anime.synonyms.length > 0) + ? anime.synonyms.join(", ") + : (anime.titleRomaji || ""); + const airdate = (anime.seasonYear && anime.season) + ? `${anime.season.charAt(0).toUpperCase() + anime.season.slice(1)} ${anime.seasonYear}` + : (anime.seasonYear || anime.status || "Unknown"); + + return JSON.stringify([{ + description: desc, + aliases: aliases, + airdate: airdate + }]); + } catch (err) { + return JSON.stringify([{ description: "Error", aliases: "Error", airdate: "Error" }]); + } +} + +// ─── 3. Extract Episodes ─── +async function extractEpisodes(url) { + try { + const anilistId = url.trim(); + + const query = `{ anime(anilistId: ${anilistId}) { id anilistId } }`; + const data = await graphqlQuery(query); + + if (!data || !data.anime || !data.anime.id) { + return JSON.stringify([{ href: "Error", number: "Error" }]); + } + + const catalogId = data.anime.id; + + const episodes = await restGet(`/episodes?id=${encodeURIComponent(catalogId)}`); + + if (!episodes || !Array.isArray(episodes)) { + return JSON.stringify([{ href: "Error", number: "Error" }]); + } + + const results = []; + for (const ep of episodes) { + results.push({ + number: ep.number || 0, + href: `${catalogId}|${ep.number}` + }); + } + + if (results.length === 0) { + return JSON.stringify([{ href: "Error", number: "Error" }]); + } + + return JSON.stringify(results); + } catch (err) { + return JSON.stringify([{ href: "Error", number: "Error" }]); + } +} + +// ─── 4. Extract Stream URL ─── +async function extractStreamUrl(url) { + try { + const parts = url.trim().split("|"); + if (parts.length < 2) { + return JSON.stringify({ streams: [], subtitles: "" }); + } + + const catalogId = parts[0]; + const epNum = parts[1]; + + const servers = await restGet(`/servers?id=${encodeURIComponent(catalogId)}&epNum=${epNum}`); + + if (!servers || !servers.subProviders) { + return JSON.stringify({ streams: [], subtitles: "" }); + } + + const streams = []; + let subtitleUrl = ""; + let subtitleHeaders = {}; + const subProviders = servers.subProviders || []; + const dubProviders = servers.dubProviders || []; + + // Process providers and collect all sources + subtitles + for (const provider of subProviders) { + try { + const sources = await restGet( + `/sources?id=${encodeURIComponent(catalogId)}&epNum=${epNum}&type=sub&providerId=${provider.id}` + ); + + // Skip if API returned an error + if (!sources || sources.error || !sources.sources || !Array.isArray(sources.sources) || sources.sources.length === 0) continue; + + // Fix Referer: ensure trailing slash + const rawReferer = (sources.headers && sources.headers.Referer) ? sources.headers.Referer : ""; + const referer = fixReferer(rawReferer); + const streamHeaders = referer ? { "Referer": referer } : {}; + + // Also copy any other useful headers (Origin, User-Agent) + if (sources.headers && sources.headers.Origin) streamHeaders["Origin"] = sources.headers.Origin; + + for (const src of sources.sources) { + var quality = src.quality || "default"; + streams.push({ + title: "SUB - " + capitalize(provider.id) + " - " + quality, + streamUrl: src.url, + headers: streamHeaders + }); + } + + // Extract subtitles: only kind=captions, prefer English default + if (sources.tracks && Array.isArray(sources.tracks)) { + var captionTracks = sources.tracks.filter(function(t) { + return t.kind === "captions" && t.url; + }); + if (captionTracks.length > 0) { + // Prefer English default track + var engDefault = captionTracks.find(function(t) { + return t.default && (t.lang || t.label || "").toLowerCase().indexOf("english") !== -1; + }); + var anyDefault = captionTracks.find(function(t) { return t.default; }); + var engTrack = captionTracks.find(function(t) { + return (t.lang || t.label || "").toLowerCase().indexOf("english") !== -1; + }); + var bestTrack = engDefault || anyDefault || engTrack || captionTracks[0]; + if (bestTrack && bestTrack.url) { + subtitleUrl = bestTrack.url; + subtitleHeaders = streamHeaders; + } + } + } + } catch (e) { + // Skip failed providers silently + } + } + + // Process dub providers + for (const provider of dubProviders) { + try { + const sources = await restGet( + `/sources?id=${encodeURIComponent(catalogId)}&epNum=${epNum}&type=dub&providerId=${provider.id}` + ); + + if (!sources || sources.error || !sources.sources || !Array.isArray(sources.sources) || sources.sources.length === 0) continue; + + const rawReferer = (sources.headers && sources.headers.Referer) ? sources.headers.Referer : ""; + const referer = fixReferer(rawReferer); + const streamHeaders = referer ? { "Referer": referer } : {}; + + if (sources.headers && sources.headers.Origin) streamHeaders["Origin"] = sources.headers.Origin; + + for (const src of sources.sources) { + var quality = src.quality || "default"; + streams.push({ + title: "DUB - " + capitalize(provider.id) + " - " + quality, + streamUrl: src.url, + headers: streamHeaders + }); + } + + // Also grab subtitles from dub providers + if (!subtitleUrl && sources.tracks && Array.isArray(sources.tracks)) { + var captionTracks = sources.tracks.filter(function(t) { + return t.kind === "captions" && t.url; + }); + if (captionTracks.length > 0) { + var anyDefault = captionTracks.find(function(t) { return t.default; }); + var bestTrack = anyDefault || captionTracks[0]; + if (bestTrack && bestTrack.url) { + subtitleUrl = bestTrack.url; + subtitleHeaders = streamHeaders; + } + } + } + } catch (e) { + // Skip failed providers silently + } + } + + if (streams.length === 0) { + return JSON.stringify({ streams: [], subtitles: "" }); + } + + return JSON.stringify({ + streams: streams, + subtitles: subtitleUrl || "" + }); + } catch (err) { + return JSON.stringify({ streams: [], subtitles: "" }); + } +} + +// ─── Utility ─── +function capitalize(str) { + if (!str) return str; + return str.charAt(0).toUpperCase() + str.slice(1); +} diff --git a/anistream/anistream.json b/anistream/anistream.json new file mode 100644 index 0000000..0e1a562 --- /dev/null +++ b/anistream/anistream.json @@ -0,0 +1,27 @@ +{ + "sourceName": "AniStream", + "iconUrl": "https://anistream.one/favicon.svg", + "author": { + "name": "Sheby" + }, + "version": "1.0.3", + "language": "Multi-Language", + "streamType": "HLS", + "quality": "1080p", + "baseUrl": "https://anistream.one/", + "searchBaseUrl": "https://graphql.animex.one/graphql", + "scriptUrl": "https://raw.githubusercontent.com/Shebyyy/Test-ext/main/anistream/anistream.js", + "type": "anime", + "asyncJS": true, + "softsub": true, + "downloadSupport": true, + "note": "Uses AniStream API with multiple servers (Sub/Dub), quality options (up to 1080p), and soft subtitles. Powered by animex.", + "supportsSora": true, + "supportsLuna": true, + "supportsDartotsu": true, + "supportsMojuru": true, + "supportsAnymex": true, + "supportsTsumi": true, + "supportsHiyoku": true, + "supportsShirox": true +}