diff --git a/manifest.json b/manifest.json index 3c7ec28..68ed857 100644 --- a/manifest.json +++ b/manifest.json @@ -1,12 +1,12 @@ { "name": "Nuvio Providers", - "version": "1.0.4", + "version": "1.0.5", "scrapers": [ { "id": "miruro", "name": "Miruro", "description": "Anime streams from Miruro with subbed and dubbed sources.", - "version": "1.0.0", + "version": "1.0.1", "author": "Jānis L", "supportedTypes": [ "movie", diff --git a/package-lock.json b/package-lock.json index fb6e59c..61d8578 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "nuvio-providers", "version": "1.0.0", "license": "GPL-3.0", + "dependencies": { + "fflate": "^0.8.3" + }, "devDependencies": { "cheerio": "^1.1.2", "cheerio-without-node-native": "^0.20.2", @@ -1124,6 +1127,12 @@ "license": "MIT", "optional": true }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", diff --git a/package.json b/package.json index 55b3798..be68ff8 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,8 @@ "streaming", "providers" ], - "license": "GPL-3.0" -} \ No newline at end of file + "license": "GPL-3.0", + "dependencies": { + "fflate": "^0.8.3" + } +} diff --git a/providers/_template.js b/providers/_template.js index 44dbc47..46ecd83 100644 --- a/providers/_template.js +++ b/providers/_template.js @@ -1,6 +1,6 @@ /** * _template - Built from src/_template/ - * Generated: 2026-07-19T17:05:24.210Z + * Generated: 2026-07-19T17:21:01.885Z */ var __create = Object.create; var __defProp = Object.defineProperty; diff --git a/providers/flixer.js b/providers/flixer.js index 3277581..aa36483 100644 --- a/providers/flixer.js +++ b/providers/flixer.js @@ -1,5 +1,763 @@ /** * flixer - Built from src/flixer/ - * Generated: 2026-07-19T17:05:50.362Z + * Generated: 2026-07-19T17:21:01.894Z */ -var se=Object.defineProperty,ie=Object.defineProperties;var ce=Object.getOwnPropertyDescriptors;var G=Object.getOwnPropertySymbols;var ae=Object.prototype.hasOwnProperty,le=Object.prototype.propertyIsEnumerable;var K=(e,r,t)=>r in e?se(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t,B=(e,r)=>{for(var t in r||(r={}))ae.call(r,t)&&K(e,t,r[t]);if(G)for(var t of G(r))le.call(r,t)&&K(e,t,r[t]);return e},S=(e,r)=>ie(e,ce(r));var m=(e,r,t)=>new Promise((n,o)=>{var c=a=>{try{i(t.next(a))}catch(h){o(h)}},s=a=>{try{i(t.throw(a))}catch(h){o(h)}},i=a=>a.done?n(a.value):Promise.resolve(a.value).then(c,s);i((t=t.apply(e,r)).next())});var $="https://flixer.gd",k="https://plsdontscrapemelove.flixer.gd",R={Accept:"text/plain",Origin:$,Referer:`${$}/`,"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36"},b="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",fe=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298];function v(e){let r=[];for(let t=0;t=55296&&n<=56319&&t+1=56320&&o<=57343&&(n=65536+(n-55296<<10)+o-56320,t++)}n<128?r.push(n):n<2048?r.push(192|n>>6,128|n&63):n<65536?r.push(224|n>>12,128|n>>6&63,128|n&63):r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|n&63)}return new Uint8Array(r)}function p(e,r){return e>>>r|e<<32-r}function E(e){let r=e.length*8,t=Math.ceil((e.length+9)/64)*64,n=new Uint8Array(t);n.set(e),n[e.length]=128;let o=Math.floor(r/4294967296),c=r>>>0,s=t-8;n[s]=o>>>24&255,n[s+1]=o>>>16&255,n[s+2]=o>>>8&255,n[s+3]=o&255,n[s+4]=c>>>24&255,n[s+5]=c>>>16&255,n[s+6]=c>>>8&255,n[s+7]=c&255;let i=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],a=new Uint32Array(64);for(let l=0;l>>0}for(let x=16;x<64;x++){let y=p(a[x-15],7)^p(a[x-15],18)^a[x-15]>>>3,L=p(a[x-2],17)^p(a[x-2],19)^a[x-2]>>>10;a[x]=a[x-16]+y+a[x-7]+L>>>0}let[A,w,C,d,f,u,g,U]=i;for(let x=0;x<64;x++){let y=p(f,6)^p(f,11)^p(f,25),L=f&u^~f&g,j=U+y+L+fe[x]+a[x]>>>0,ne=p(A,2)^p(A,13)^p(A,22),re=A&w^A&C^w&C,oe=ne+re>>>0;U=g,g=u,u=f,f=d+j>>>0,d=C,C=w,w=A,A=j+oe>>>0}i[0]=i[0]+A>>>0,i[1]=i[1]+w>>>0,i[2]=i[2]+C>>>0,i[3]=i[3]+d>>>0,i[4]=i[4]+f>>>0,i[5]=i[5]+u>>>0,i[6]=i[6]+g>>>0,i[7]=i[7]+U>>>0}let h=new Uint8Array(32);for(let l=0;l>>24&255,h[l*4+1]=i[l]>>>16&255,h[l*4+2]=i[l]>>>8&255,h[l*4+3]=i[l]&255;return h}function P(e,r){let t=e instanceof Uint8Array?e:v(String(e));t.length>64&&(t=E(t));let n=r instanceof Uint8Array?r:v(String(r)),o=new Uint8Array(64+n.length),c=new Uint8Array(96);for(let s=0;s<64;s++){let i=s>>18&63],r+=b[c>>>12&63],r+=n?b[c>>>6&63]:"=",r+=o?b[c&63]:"="}return r}function ue(){let e=new Uint8Array(16);for(let r=0;r>>10,56320|o&1023))}return r}function D(e,r){let t=0;for(let n=0;n<8;n++)r&1&&(t^=e),e=(e<<1^(e&128?283:0))&255,r>>>=1;return t}function me(e){if(!e)return 0;let r=1,t=e,n=254;for(;n;)n&1&&(r=D(r,t)),t=D(t,t),n>>>=1;return r}function M(e,r){return(e<>>8-r)&255}var F=(()=>{let e=new Uint8Array(256);for(let r=0;r<256;r++){let t=me(r);e[r]=t^M(t,1)^M(t,2)^M(t,3)^M(t,4)^99}return e})();function Ce(e){if(e.length!==32)throw new Error("Invalid AES-256 key");let r=new Uint32Array(60);for(let n=0;n<8;n++){let o=n*4;r[n]=(e[o]<<24|e[o+1]<<16|e[o+2]<<8|e[o+3])>>>0}let t=1;for(let n=8;n>>24)>>>0,o=(F[o>>>24]<<24|F[o>>>16&255]<<16|F[o>>>8&255]<<8|F[o&255])>>>0,o^=t<<24,t=(t<<1^(t&128?283:0))&255):n%8===4&&(o=(F[o>>>24]<<24|F[o>>>16&255]<<16|F[o>>>8&255]<<8|F[o&255])>>>0),r[n]=(r[n-8]^o)>>>0}return r}function W(e,r,t){for(let n=0;n<4;n++){let o=r[t*4+n];e[n*4]^=o>>>24,e[n*4+1]^=o>>>16&255,e[n*4+2]^=o>>>8&255,e[n*4+3]^=o&255}}function O(e,r){let t=new Uint8Array(e),n=new Uint8Array(16);W(t,r,0);for(let o=1;o<=14;o++){for(let c=0;c<16;c++)t[c]=F[t[c]];for(let c=0;c<4;c++)for(let s=0;s<4;s++)n[c+s*4]=t[c+(s+c&3)*4];if(t.set(n),o!==14)for(let c=0;c<4;c++){let s=c*4,i=t[s],a=t[s+1],h=t[s+2],l=t[s+3],A=i^a^h^l;t[s]^=A^D(i^a,2),t[s+1]^=A^D(a^h,2),t[s+2]^=A^D(h^l,2),t[s+3]^=A^D(l^i,2)}W(t,r,o)}return t}function X(e){let r=new Uint32Array(4);for(let t=0;t<4;t++){let n=t*4;r[t]=(e[n]<<24|e[n+1]<<16|e[n+2]<<8|e[n+3])>>>0}return r}function we(e){let r=new Uint8Array(16);for(let t=0;t<4;t++)r[t*4]=e[t]>>>24,r[t*4+1]=e[t]>>>16&255,r[t*4+2]=e[t]>>>8&255,r[t*4+3]=e[t]&255;return r}function V(e,r){let t=new Uint32Array(4),n=new Uint32Array(r);for(let o=0;o<128;o++){if(e[o>>>5]>>>31-(o&31)&1)for(let s=0;s<4;s++)t[s]^=n[s];let c=n[3]&1;n[3]=(n[3]>>>1|(n[2]&1)<<31)>>>0,n[2]=(n[2]>>>1|(n[1]&1)<<31)>>>0,n[1]=(n[1]>>>1|(n[0]&1)<<31)>>>0,n[0]>>>=1,c&&(n[0]=(n[0]^3774873600)>>>0)}return t}function pe(e,r){let t=new Uint32Array(4);for(let i=0;i>>0),c.setUint32(8,Math.floor(o/4294967296));let s=X(n);for(let i=0;i<4;i++)t[i]^=s[i];return V(t,r)}function Fe(e){for(let r=15;r>=12&&(e[r]=e[r]+1&255,!e[r]);r--);}function ye(e,r){if(e.length<28)throw new Error("Invalid encrypted payload");let t=e.subarray(0,12),n=e.subarray(12,e.length-16),o=e.subarray(e.length-16),c=Ce(r),s=new Uint8Array(16),i=X(O(s,c)),a=new Uint8Array(16);a.set(t),a[15]=1;let h=we(pe(n,i)),l=O(a,c),A=0;for(let d=0;d<16;d++)A|=h[d]^l[d]^o[d];if(A)throw new Error("Invalid encrypted payload tag");let w=new Uint8Array(n.length),C=new Uint8Array(a);for(let d=0;dr.includes(t)).concat(r.filter(t=>!Z.includes(t)))}function De(e,r){if(e&&Array.isArray(e.sources)){let n=e.sources.find(o=>o&&String(o.server||"").toLowerCase()===r)||e.sources.find(o=>o&&o.url);return n&&typeof n.url=="string"?n.url.trim():""}if(e&&e.sources&&typeof e.sources=="object"){let t=e.sources.file||e.sources.url;return typeof t=="string"?t.trim():""}return""}function $e(e){let r=String(e).match(/(?:^|[^0-9])(2160|1440|1080|720|480|360)p?(?:[^0-9]|$)/i);return r?`${r[1]}p`:"Auto"}function be(e,r,t,n){return m(this,null,function*(){let o=yield H(t,r);return yield _(e,{headers:S(B({},o),{"X-Only-Sources":"1","X-Server":n})})})}function te(e,r,t,n){return m(this,null,function*(){let o=String(e||"").trim();if(r!=="movie"&&r!=="tv")return[];if(r==="tv"&&(!Number.isInteger(Number(t))||!Number.isInteger(Number(n))||Number(t)<1||Number(n)<1))return[];let c=yield Ue(o,r);if(!c)return[];J();let s=Q(),i=Se(c,r,Number(t),Number(n)),a=`${k}${i}`,h=yield H(s,i),l=yield _(a,{headers:S(B({},h),{bW90aGFmYWth:"1"})}),A=ve(yield ee(l,s)),w=yield Promise.all(A.map(f=>m(this,null,function*(){try{return{server:f,encrypted:yield be(a,i,s,f)}}catch(u){return console.warn(`[Flixer] ${f} request failed: ${u&&u.message?u.message:String(u)}`),{server:f,encrypted:""}}}))),C=[],d={};for(let f of w)if(f.encrypted)try{let u=De(yield ee(f.encrypted,s),f.server);if(!u||d[u])continue;d[u]=!0;let g=Ee[f.server]||f.server.charAt(0).toUpperCase()+f.server.slice(1),U=$e(u);C.push({name:`Flixer \xB7 ${g}`,title:`${g} \xB7 ${U}`,url:u,quality:U,headers:{Referer:`${$}/`,Origin:$,"User-Agent":R["User-Agent"]}})}catch(u){let g=u&&u.message?u.message:String(u);/^E5[678]$/.test(g)||console.warn(`[Flixer] ${f.server} decode failed: ${g}`)}return C})}function Re(e,r,t,n){return m(this,null,function*(){try{console.log(`[Flixer] Request: ${r} ${e}`);let o=yield te(e,r,t,n);return console.log(`[Flixer] Found ${o.length} stream(s)`),o}catch(o){return console.error(`[Flixer] Error: ${o.message}`),[]}})}module.exports={getStreams:Re}; +var __defProp = Object.defineProperty; +var __defProps = Object.defineProperties; +var __getOwnPropDescs = Object.getOwnPropertyDescriptors; +var __getOwnPropSymbols = Object.getOwnPropertySymbols; +var __hasOwnProp = Object.prototype.hasOwnProperty; +var __propIsEnum = Object.prototype.propertyIsEnumerable; +var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; +var __spreadValues = (a, b) => { + for (var prop in b || (b = {})) + if (__hasOwnProp.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + if (__getOwnPropSymbols) + for (var prop of __getOwnPropSymbols(b)) { + if (__propIsEnum.call(b, prop)) + __defNormalProp(a, prop, b[prop]); + } + return a; +}; +var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b)); +var __async = (__this, __arguments, generator) => { + return new Promise((resolve, reject) => { + var fulfilled = (value) => { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + }; + var rejected = (value) => { + try { + step(generator.throw(value)); + } catch (e) { + reject(e); + } + }; + var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected); + step((generator = generator.apply(__this, __arguments)).next()); + }); +}; + +// src/flixer/http.js +var SITE_URL = "https://flixer.gd"; +var API_URL = "https://plsdontscrapemelove.flixer.gd"; +var HEADERS = { + "Accept": "text/plain", + "Origin": SITE_URL, + "Referer": `${SITE_URL}/`, + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36" +}; +var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +var SHA256_CONSTANTS = [ + 1116352408, + 1899447441, + 3049323471, + 3921009573, + 961987163, + 1508970993, + 2453635748, + 2870763221, + 3624381080, + 310598401, + 607225278, + 1426881987, + 1925078388, + 2162078206, + 2614888103, + 3248222580, + 3835390401, + 4022224774, + 264347078, + 604807628, + 770255983, + 1249150122, + 1555081692, + 1996064986, + 2554220882, + 2821834349, + 2952996808, + 3210313671, + 3336571891, + 3584528711, + 113926993, + 338241895, + 666307205, + 773529912, + 1294757372, + 1396182291, + 1695183700, + 1986661051, + 2177026350, + 2456956037, + 2730485921, + 2820302411, + 3259730800, + 3345764771, + 3516065817, + 3600352804, + 4094571909, + 275423344, + 430227734, + 506948616, + 659060556, + 883997877, + 958139571, + 1322822218, + 1537002063, + 1747873779, + 1955562222, + 2024104815, + 2227730452, + 2361852424, + 2428436474, + 2756734187, + 3204031479, + 3329325298 +]; +function encodeUtf8(value) { + const bytes = []; + for (let index = 0; index < value.length; index++) { + let codePoint = value.charCodeAt(index); + if (codePoint >= 55296 && codePoint <= 56319 && index + 1 < value.length) { + const trailing = value.charCodeAt(index + 1); + if (trailing >= 56320 && trailing <= 57343) { + codePoint = 65536 + (codePoint - 55296 << 10) + trailing - 56320; + index++; + } + } + if (codePoint < 128) + bytes.push(codePoint); + else if (codePoint < 2048) + bytes.push(192 | codePoint >> 6, 128 | codePoint & 63); + else if (codePoint < 65536) + bytes.push(224 | codePoint >> 12, 128 | codePoint >> 6 & 63, 128 | codePoint & 63); + else + bytes.push(240 | codePoint >> 18, 128 | codePoint >> 12 & 63, 128 | codePoint >> 6 & 63, 128 | codePoint & 63); + } + return new Uint8Array(bytes); +} +function rotateRight(value, amount) { + return value >>> amount | value << 32 - amount; +} +function sha256(input) { + const bitLength = input.length * 8; + const paddedLength = Math.ceil((input.length + 9) / 64) * 64; + const bytes = new Uint8Array(paddedLength); + bytes.set(input); + bytes[input.length] = 128; + const high = Math.floor(bitLength / 4294967296); + const low = bitLength >>> 0; + const end = paddedLength - 8; + bytes[end] = high >>> 24 & 255; + bytes[end + 1] = high >>> 16 & 255; + bytes[end + 2] = high >>> 8 & 255; + bytes[end + 3] = high & 255; + bytes[end + 4] = low >>> 24 & 255; + bytes[end + 5] = low >>> 16 & 255; + bytes[end + 6] = low >>> 8 & 255; + bytes[end + 7] = low & 255; + const hash = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225]; + const words = new Uint32Array(64); + for (let offset = 0; offset < bytes.length; offset += 64) { + for (let index = 0; index < 16; index++) { + const position = offset + index * 4; + words[index] = (bytes[position] << 24 | bytes[position + 1] << 16 | bytes[position + 2] << 8 | bytes[position + 3]) >>> 0; + } + for (let index = 16; index < 64; index++) { + const first = rotateRight(words[index - 15], 7) ^ rotateRight(words[index - 15], 18) ^ words[index - 15] >>> 3; + const second = rotateRight(words[index - 2], 17) ^ rotateRight(words[index - 2], 19) ^ words[index - 2] >>> 10; + words[index] = words[index - 16] + first + words[index - 7] + second >>> 0; + } + let [a, b, c, d, e, f, g, h] = hash; + for (let index = 0; index < 64; index++) { + const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25); + const choice = e & f ^ ~e & g; + const temporary1 = h + sum1 + choice + SHA256_CONSTANTS[index] + words[index] >>> 0; + const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22); + const majority = a & b ^ a & c ^ b & c; + const temporary2 = sum0 + majority >>> 0; + h = g; + g = f; + f = e; + e = d + temporary1 >>> 0; + d = c; + c = b; + b = a; + a = temporary1 + temporary2 >>> 0; + } + hash[0] = hash[0] + a >>> 0; + hash[1] = hash[1] + b >>> 0; + hash[2] = hash[2] + c >>> 0; + hash[3] = hash[3] + d >>> 0; + hash[4] = hash[4] + e >>> 0; + hash[5] = hash[5] + f >>> 0; + hash[6] = hash[6] + g >>> 0; + hash[7] = hash[7] + h >>> 0; + } + const output = new Uint8Array(32); + for (let index = 0; index < hash.length; index++) { + output[index * 4] = hash[index] >>> 24 & 255; + output[index * 4 + 1] = hash[index] >>> 16 & 255; + output[index * 4 + 2] = hash[index] >>> 8 & 255; + output[index * 4 + 3] = hash[index] & 255; + } + return output; +} +function hmacSha256(key, message) { + let keyBytes = key instanceof Uint8Array ? key : encodeUtf8(String(key)); + if (keyBytes.length > 64) + keyBytes = sha256(keyBytes); + const messageBytes = message instanceof Uint8Array ? message : encodeUtf8(String(message)); + const inner = new Uint8Array(64 + messageBytes.length); + const outer = new Uint8Array(96); + for (let index = 0; index < 64; index++) { + const value = index < keyBytes.length ? keyBytes[index] : 0; + inner[index] = value ^ 54; + outer[index] = value ^ 92; + } + inner.set(messageBytes, 64); + outer.set(sha256(inner), 64); + return sha256(outer); +} +function encodeBase64(bytes) { + let output = ""; + for (let index = 0; index < bytes.length; index += 3) { + const secondExists = index + 1 < bytes.length; + const thirdExists = index + 2 < bytes.length; + const value = bytes[index] << 16 | (secondExists ? bytes[index + 1] : 0) << 8 | (thirdExists ? bytes[index + 2] : 0); + output += BASE64_ALPHABET[value >>> 18 & 63]; + output += BASE64_ALPHABET[value >>> 12 & 63]; + output += secondExists ? BASE64_ALPHABET[value >>> 6 & 63] : "="; + output += thirdExists ? BASE64_ALPHABET[value & 63] : "="; + } + return output; +} +function createNonce() { + const bytes = new Uint8Array(16); + for (let index = 0; index < bytes.length; index++) + bytes[index] = Math.floor(Math.random() * 256); + return encodeBase64(bytes).replace(/[/+=]/g, "").slice(0, 22); +} +var serverTimeOffset = 0; +var serverTimeFetchedAt = 0; +function getServerTimestamp() { + return __async(this, null, function* () { + const now = Date.now(); + if (serverTimeFetchedAt && now - serverTimeFetchedAt < 3e5) { + return Math.floor((now + serverTimeOffset) / 1e3); + } + try { + const startedAt = Date.now(); + const response = yield fetch(`${API_URL}/api/time?t=${startedAt}`, { + headers: __spreadProps(__spreadValues({}, HEADERS), { "Cache-Control": "no-cache" }) + }); + if (!response.ok) + throw new Error(`HTTP ${response.status}`); + const payload = yield response.json(); + if (!payload || typeof payload.timestamp !== "number") + throw new Error("invalid response"); + const finishedAt = Date.now(); + serverTimeOffset = payload.timestamp * 1e3 + (finishedAt - startedAt) / 2 - finishedAt; + serverTimeFetchedAt = finishedAt; + return Math.floor((finishedAt + serverTimeOffset) / 1e3); + } catch (_) { + return Math.floor(Date.now() / 1e3); + } + }); +} +function createSecureHeaders(apiKey, path) { + return __async(this, null, function* () { + const timestamp = yield getServerTimestamp(); + const nonce = createNonce(); + const message = `${apiKey}:${timestamp}:${nonce}:${path}`; + return { + "X-Api-Key": apiKey, + "X-Request-Timestamp": String(timestamp), + "X-Request-Nonce": nonce, + "X-Request-Signature": encodeBase64(hmacSha256(apiKey, message)), + "X-Client-Fingerprint": "zerlgt", + "X-Fingerprint-Lite": "e9136c41504646444" + }; + }); +} +function fetchText(_0) { + return __async(this, arguments, function* (url, options = {}) { + const response = yield fetch(url, __spreadProps(__spreadValues({}, options), { + headers: __spreadValues(__spreadValues({}, HEADERS), options.headers || {}) + })); + if (!response.ok) + throw new Error(`HTTP ${response.status} for ${url}`); + return yield response.text(); + }); +} + +// src/flixer/decoder.js +var CANVAS_FINGERPRINT = "iVBORw0KGgoAAAANSUhEUgAAAMgAAAAyCAYAAAAZUZThAAAQAE"; +function concatBytes(...values) { + let length = 0; + for (const value of values) + length += value.length; + const result = new Uint8Array(length); + let offset = 0; + for (const value of values) { + result.set(value, offset); + offset += value.length; + } + return result; +} +function toHex(bytes) { + let result = ""; + for (const value of bytes) + result += value.toString(16).padStart(2, "0"); + return result; +} +function decodeBase64(value) { + const binary = atob(value); + const bytes = new Uint8Array(binary.length); + for (let index = 0; index < binary.length; index++) + bytes[index] = binary.charCodeAt(index); + return bytes; +} +function decodeUtf8(bytes) { + let result = ""; + for (let index = 0; index < bytes.length; ) { + const first = bytes[index++]; + let codePoint; + if (first < 128) + codePoint = first; + else if ((first & 224) === 192) + codePoint = (first & 31) << 6 | bytes[index++] & 63; + else if ((first & 240) === 224) { + codePoint = (first & 15) << 12 | (bytes[index++] & 63) << 6 | bytes[index++] & 63; + } else { + codePoint = (first & 7) << 18 | (bytes[index++] & 63) << 12 | (bytes[index++] & 63) << 6 | bytes[index++] & 63; + } + if (codePoint <= 65535) + result += String.fromCharCode(codePoint); + else { + codePoint -= 65536; + result += String.fromCharCode(55296 | codePoint >>> 10, 56320 | codePoint & 1023); + } + } + return result; +} +function multiplyByte(first, second) { + let result = 0; + for (let index = 0; index < 8; index++) { + if (second & 1) + result ^= first; + first = (first << 1 ^ (first & 128 ? 283 : 0)) & 255; + second >>>= 1; + } + return result; +} +function inverseByte(value) { + if (!value) + return 0; + let result = 1; + let base = value; + let power = 254; + while (power) { + if (power & 1) + result = multiplyByte(result, base); + base = multiplyByte(base, base); + power >>>= 1; + } + return result; +} +function rotateByte(value, amount) { + return (value << amount | value >>> 8 - amount) & 255; +} +var AES_SBOX = (() => { + const result = new Uint8Array(256); + for (let index = 0; index < 256; index++) { + const value = inverseByte(index); + result[index] = value ^ rotateByte(value, 1) ^ rotateByte(value, 2) ^ rotateByte(value, 3) ^ rotateByte(value, 4) ^ 99; + } + return result; +})(); +function expandAes256Key(key) { + if (key.length !== 32) + throw new Error("Invalid AES-256 key"); + const words = new Uint32Array(60); + for (let index = 0; index < 8; index++) { + const offset = index * 4; + words[index] = (key[offset] << 24 | key[offset + 1] << 16 | key[offset + 2] << 8 | key[offset + 3]) >>> 0; + } + let roundConstant = 1; + for (let index = 8; index < words.length; index++) { + let temporary = words[index - 1]; + if (index % 8 === 0) { + temporary = (temporary << 8 | temporary >>> 24) >>> 0; + temporary = (AES_SBOX[temporary >>> 24] << 24 | AES_SBOX[temporary >>> 16 & 255] << 16 | AES_SBOX[temporary >>> 8 & 255] << 8 | AES_SBOX[temporary & 255]) >>> 0; + temporary ^= roundConstant << 24; + roundConstant = (roundConstant << 1 ^ (roundConstant & 128 ? 283 : 0)) & 255; + } else if (index % 8 === 4) { + temporary = (AES_SBOX[temporary >>> 24] << 24 | AES_SBOX[temporary >>> 16 & 255] << 16 | AES_SBOX[temporary >>> 8 & 255] << 8 | AES_SBOX[temporary & 255]) >>> 0; + } + words[index] = (words[index - 8] ^ temporary) >>> 0; + } + return words; +} +function addRoundKey(state, words, round) { + for (let column = 0; column < 4; column++) { + const word = words[round * 4 + column]; + state[column * 4] ^= word >>> 24; + state[column * 4 + 1] ^= word >>> 16 & 255; + state[column * 4 + 2] ^= word >>> 8 & 255; + state[column * 4 + 3] ^= word & 255; + } +} +function encryptAesBlock(input, words) { + const state = new Uint8Array(input); + const shifted = new Uint8Array(16); + addRoundKey(state, words, 0); + for (let round = 1; round <= 14; round++) { + for (let index = 0; index < 16; index++) + state[index] = AES_SBOX[state[index]]; + for (let row = 0; row < 4; row++) { + for (let column = 0; column < 4; column++) { + shifted[row + column * 4] = state[row + (column + row & 3) * 4]; + } + } + state.set(shifted); + if (round !== 14) { + for (let column = 0; column < 4; column++) { + const offset = column * 4; + const first = state[offset]; + const second = state[offset + 1]; + const third = state[offset + 2]; + const fourth = state[offset + 3]; + const combined = first ^ second ^ third ^ fourth; + state[offset] ^= combined ^ multiplyByte(first ^ second, 2); + state[offset + 1] ^= combined ^ multiplyByte(second ^ third, 2); + state[offset + 2] ^= combined ^ multiplyByte(third ^ fourth, 2); + state[offset + 3] ^= combined ^ multiplyByte(fourth ^ first, 2); + } + } + addRoundKey(state, words, round); + } + return state; +} +function bytesToWords(bytes) { + const result = new Uint32Array(4); + for (let index = 0; index < 4; index++) { + const offset = index * 4; + result[index] = (bytes[offset] << 24 | bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3]) >>> 0; + } + return result; +} +function wordsToBytes(words) { + const result = new Uint8Array(16); + for (let index = 0; index < 4; index++) { + result[index * 4] = words[index] >>> 24; + result[index * 4 + 1] = words[index] >>> 16 & 255; + result[index * 4 + 2] = words[index] >>> 8 & 255; + result[index * 4 + 3] = words[index] & 255; + } + return result; +} +function multiplyGhash(first, second) { + const result = new Uint32Array(4); + const value = new Uint32Array(second); + for (let bit = 0; bit < 128; bit++) { + if (first[bit >>> 5] >>> 31 - (bit & 31) & 1) { + for (let index = 0; index < 4; index++) + result[index] ^= value[index]; + } + const lowBit = value[3] & 1; + value[3] = (value[3] >>> 1 | (value[2] & 1) << 31) >>> 0; + value[2] = (value[2] >>> 1 | (value[1] & 1) << 31) >>> 0; + value[1] = (value[1] >>> 1 | (value[0] & 1) << 31) >>> 0; + value[0] >>>= 1; + if (lowBit) + value[0] = (value[0] ^ 3774873600) >>> 0; + } + return result; +} +function calculateGhash(ciphertext, hashKey) { + let state = new Uint32Array(4); + for (let offset = 0; offset < ciphertext.length; offset += 16) { + const block = new Uint8Array(16); + block.set(ciphertext.subarray(offset, Math.min(offset + 16, ciphertext.length))); + const words = bytesToWords(block); + for (let index = 0; index < 4; index++) + state[index] ^= words[index]; + state = multiplyGhash(state, hashKey); + } + const lengthBlock = new Uint8Array(16); + const bitLength = ciphertext.length * 8; + const view = new DataView(lengthBlock.buffer); + view.setUint32(12, bitLength >>> 0); + view.setUint32(8, Math.floor(bitLength / 4294967296)); + const lengths = bytesToWords(lengthBlock); + for (let index = 0; index < 4; index++) + state[index] ^= lengths[index]; + return multiplyGhash(state, hashKey); +} +function incrementCounter(counter) { + for (let index = 15; index >= 12; index--) { + counter[index] = counter[index] + 1 & 255; + if (counter[index]) + break; + } +} +function decryptAesGcm(payload, key) { + if (payload.length < 28) + throw new Error("Invalid encrypted payload"); + const nonce = payload.subarray(0, 12); + const ciphertext = payload.subarray(12, payload.length - 16); + const expectedTag = payload.subarray(payload.length - 16); + const words = expandAes256Key(key); + const zero = new Uint8Array(16); + const hashKey = bytesToWords(encryptAesBlock(zero, words)); + const initialCounter = new Uint8Array(16); + initialCounter.set(nonce); + initialCounter[15] = 1; + const authentication = wordsToBytes(calculateGhash(ciphertext, hashKey)); + const encryptedCounter = encryptAesBlock(initialCounter, words); + let mismatch = 0; + for (let index = 0; index < 16; index++) + mismatch |= authentication[index] ^ encryptedCounter[index] ^ expectedTag[index]; + if (mismatch) + throw new Error("Invalid encrypted payload tag"); + const output = new Uint8Array(ciphertext.length); + const counter = new Uint8Array(initialCounter); + for (let offset = 0; offset < ciphertext.length; offset += 16) { + incrementCounter(counter); + const stream = encryptAesBlock(counter, words); + const length = Math.min(16, ciphertext.length - offset); + for (let index = 0; index < length; index++) + output[offset + index] = ciphertext[offset + index] ^ stream[index]; + } + return output; +} +function derivePayloadKey(apiKey, now) { + const hour = Math.floor(now / 36e5); + const dynamic = new Uint8Array(16); + for (let index = 0; index < dynamic.length; index++) { + dynamic[index] = index + 1 ^ Math.floor(hour / 2 ** (index & 7)) & 255; + } + let derived = concatBytes(encodeUtf8(apiKey), dynamic); + for (let iteration = 0; iteration < 600; iteration++) + derived = sha256(derived); + const timeBucket = Math.floor(now / 3e5); + return hmacSha256(derived, `${timeBucket}${apiKey}`); +} +function initDecoder() { + return true; +} +function get_img_key() { + const timestamp = Math.floor(Date.now() / 1e3); + const fingerprint = `1365x768:24:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36:Linux x86_64:en-US:-180:${timestamp}:${CANVAS_FINGERPRINT}`; + return toHex(sha256(encodeUtf8(toHex(sha256(encodeUtf8(fingerprint)))))); +} +function process_img_data(value, apiKey) { + const key = derivePayloadKey(apiKey, Date.now()); + return decodeUtf8(decryptAesGcm(decodeBase64(value), key)); +} + +// src/flixer/extractor.js +var NATO_ORDER = [ + "alpha", + "bravo", + "charlie", + "delta", + "echo", + "foxtrot", + "golf", + "hotel", + "india", + "juliet", + "kilo", + "lima", + "mike", + "november", + "oscar", + "papa", + "quebec", + "romeo", + "sierra", + "tango", + "uniform", + "victor", + "whiskey", + "xray", + "yankee", + "zulu" +]; +var SERVER_LABELS = { + bravo: "Balder", + delta: "Dionysus", + echo: "Eros", + golf: "Gaia", + hotel: "Hades", + india: "Isis", + juliet: "Juno", + kilo: "Kronos", + lima: "Loki" +}; +var TMDB_ID_CACHE = {}; +function resolveTmdbId(id, mediaType) { + return __async(this, null, function* () { + if (/^\d+$/.test(id)) + return id; + if (!/^tt\d+$/i.test(id)) + return null; + const cacheKey = `${mediaType}:${id.toLowerCase()}`; + if (TMDB_ID_CACHE[cacheKey]) + return TMDB_ID_CACHE[cacheKey]; + const catalogType = mediaType === "movie" ? "movie" : "series"; + try { + const raw = yield fetchText(`https://v3-cinemeta.strem.io/meta/${catalogType}/${encodeURIComponent(id)}.json`, { + headers: { "Accept": "application/json" } + }); + const payload = JSON.parse(raw); + const resolved = payload && payload.meta && payload.meta.moviedb_id; + if (!resolved || !/^\d+$/.test(String(resolved))) + return null; + TMDB_ID_CACHE[cacheKey] = String(resolved); + console.log(`[Flixer] Resolved ${id} to TMDB ${resolved}`); + return TMDB_ID_CACHE[cacheKey]; + } catch (error) { + console.warn(`[Flixer] Could not resolve IMDb ID ${id}: ${error && error.message ? error.message : String(error)}`); + return null; + } + }); +} +function buildApiPath(tmdbId, mediaType, season, episode) { + if (mediaType === "movie") + return `/api/tmdb/movie/${encodeURIComponent(tmdbId)}/images`; + return `/api/tmdb/tv/${encodeURIComponent(tmdbId)}/season/${season}/episode/${episode}/images`; +} +function decryptPayload(encrypted, apiKey) { + return __async(this, null, function* () { + const clearText = yield process_img_data(encrypted, apiKey); + return JSON.parse(clearText); + }); +} +function getServers(payload) { + const values = []; + if (payload && Array.isArray(payload.sources)) { + for (const source of payload.sources) { + const server = source && String(source.server || "").toLowerCase(); + if (server && !values.includes(server)) + values.push(server); + } + } + if (payload && payload.servers && typeof payload.servers === "object") { + for (const server of Object.keys(payload.servers)) { + const normalized = server.toLowerCase(); + if (!values.includes(normalized)) + values.push(normalized); + } + } + return NATO_ORDER.filter((server) => values.includes(server)).concat(values.filter((server) => !NATO_ORDER.includes(server))); +} +function findUrl(payload, server) { + if (payload && Array.isArray(payload.sources)) { + const exact = payload.sources.find((source) => source && String(source.server || "").toLowerCase() === server); + const candidate = exact || payload.sources.find((source) => source && source.url); + return candidate && typeof candidate.url === "string" ? candidate.url.trim() : ""; + } + if (payload && payload.sources && typeof payload.sources === "object") { + const value = payload.sources.file || payload.sources.url; + return typeof value === "string" ? value.trim() : ""; + } + return ""; +} +function getQuality(url) { + const match = String(url).match(/(?:^|[^0-9])(2160|1440|1080|720|480|360)p?(?:[^0-9]|$)/i); + return match ? `${match[1]}p` : "Auto"; +} +function fetchServerPayload(apiUrl, apiPath, apiKey, server) { + return __async(this, null, function* () { + const secureHeaders = yield createSecureHeaders(apiKey, apiPath); + return yield fetchText(apiUrl, { + headers: __spreadProps(__spreadValues({}, secureHeaders), { + "X-Only-Sources": "1", + "X-Server": server + }) + }); + }); +} +function extractStreams(tmdbId, mediaType, season, episode) { + return __async(this, null, function* () { + const id = String(tmdbId || "").trim(); + if (mediaType !== "movie" && mediaType !== "tv") + return []; + if (mediaType === "tv" && (!Number.isInteger(Number(season)) || !Number.isInteger(Number(episode)) || Number(season) < 1 || Number(episode) < 1)) + return []; + const resolvedId = yield resolveTmdbId(id, mediaType); + if (!resolvedId) + return []; + initDecoder(); + const apiKey = get_img_key(); + const apiPath = buildApiPath(resolvedId, mediaType, Number(season), Number(episode)); + const apiUrl = `${API_URL}${apiPath}`; + const secureHeaders = yield createSecureHeaders(apiKey, apiPath); + const serverPayload = yield fetchText(apiUrl, { + headers: __spreadProps(__spreadValues({}, secureHeaders), { bW90aGFmYWth: "1" }) + }); + const servers = getServers(yield decryptPayload(serverPayload, apiKey)); + const encryptedResponses = yield Promise.all(servers.map((server) => __async(this, null, function* () { + try { + return { server, encrypted: yield fetchServerPayload(apiUrl, apiPath, apiKey, server) }; + } catch (error) { + console.warn(`[Flixer] ${server} request failed: ${error && error.message ? error.message : String(error)}`); + return { server, encrypted: "" }; + } + }))); + const streams = []; + const seenUrls = {}; + for (const response of encryptedResponses) { + if (!response.encrypted) + continue; + try { + const url = findUrl(yield decryptPayload(response.encrypted, apiKey), response.server); + if (!url || seenUrls[url]) + continue; + seenUrls[url] = true; + const label = SERVER_LABELS[response.server] || response.server.charAt(0).toUpperCase() + response.server.slice(1); + const quality = getQuality(url); + streams.push({ + name: `Flixer \xB7 ${label}`, + title: `${label} \xB7 ${quality}`, + url, + quality, + headers: { + "Referer": `${SITE_URL}/`, + "Origin": SITE_URL, + "User-Agent": HEADERS["User-Agent"] + } + }); + } catch (error) { + const message = error && error.message ? error.message : String(error); + if (!/^E5[678]$/.test(message)) { + console.warn(`[Flixer] ${response.server} decode failed: ${message}`); + } + } + } + return streams; + }); +} + +// src/flixer/index.js +function getStreams(tmdbId, mediaType, season, episode) { + return __async(this, null, function* () { + try { + console.log(`[Flixer] Request: ${mediaType} ${tmdbId}`); + const streams = yield extractStreams(tmdbId, mediaType, season, episode); + console.log(`[Flixer] Found ${streams.length} stream(s)`); + return streams; + } catch (error) { + console.error(`[Flixer] Error: ${error.message}`); + return []; + } + }); +} +module.exports = { getStreams }; diff --git a/providers/miruro.js b/providers/miruro.js index 8744f4c..0edb841 100644 --- a/providers/miruro.js +++ b/providers/miruro.js @@ -1,6 +1,6 @@ /** * miruro - Built from src/miruro/ - * Generated: 2026-07-19T17:01:21.823Z + * Generated: 2026-07-19T17:21:26.205Z */ var __defProp = Object.defineProperty; var __defProps = Object.defineProperties; @@ -573,6 +573,7 @@ function secureGet(_0) { // src/miruro/extractor.js var ANIMAP_URL = "https://animap.id/api/map"; var ANILIST_URL = "https://graphql.anilist.co"; +var HLS_QUALITY_TIMEOUT_MS = 4e3; var PROVIDERS = [ { id: "bonk", label: "Bonk", subCategory: "ssub" }, { id: "kiwi", label: "Kiwi", subCategory: "sub" }, @@ -762,38 +763,88 @@ function getOrigin(url) { const match = typeof url === "string" ? url.match(/^(https?:\/\/[^/]+)/i) : null; return match ? match[1] : BASE_URL; } -function getQuality(stream) { - const value = String(stream.quality || stream.server || "HD"); +function getDeclaredQuality(stream) { + const value = `${stream.quality || ""} ${stream.resolution || ""} ${stream.server || ""}`; const resolution = value.match(/(2160|1440|1080|720|480|360)p?/i); if (resolution) { return `${resolution[1]}p`; } - return /4k/i.test(value) ? "4K" : "HD"; + return /4k/i.test(value) ? "4K" : null; +} +function getHlsQuality(manifest) { + const heights = []; + const pattern = /\bRESOLUTION\s*=\s*\d+\s*x\s*(\d+)\b/gi; + let match; + while ((match = pattern.exec(manifest)) !== null) { + const height = Number(match[1]); + if (height > 0) { + heights.push(height); + } + } + return heights.length > 0 ? `${Math.max(...heights)}p` : null; +} +function isHlsStream(stream) { + const type = String(stream && stream.type || "").toLowerCase(); + return type === "hls" || type === "m3u8" || type === "application/x-mpegurl"; +} +function resolveQuality(stream, headers) { + return __async(this, null, function* () { + const declaredQuality = getDeclaredQuality(stream); + if (declaredQuality) { + return declaredQuality; + } + if (isHlsStream(stream)) { + let timeoutId; + try { + const manifest = yield Promise.race([ + fetchText(stream.url, { headers }), + new Promise((resolve) => { + timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS); + }) + ]); + const hlsQuality = typeof manifest === "string" ? getHlsQuality(manifest) : null; + if (hlsQuality) { + return hlsQuality; + } + } catch (_) { + } finally { + if (timeoutId != null) { + clearTimeout(timeoutId); + } + } + } + return "HD"; + }); } function isPlayableStream(stream) { const type = String(stream && stream.type || "").toLowerCase(); return !!(stream && stream.url) && (type === "hls" || type === "m3u8" || type === "mp4" || type === "application/x-mpegurl"); } function normalizeStreams(sourcePayload, provider, variant) { - const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; - const language = variant === "dub" ? "DUB" : "SUB"; - return (sourcePayload.streams || []).filter(isPlayableStream).map((stream) => { - const referer = stream.referer || BASE_URL; - const server = stream.server || getQuality(stream); - const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : ""; - return { - // Nuvio's source picker prominently displays `name`, so include the - // Miruro server and audio variant here instead of only in `title`. - name: `Miruro \xB7 ${provider.label} \xB7 ${language}`, - title: `[${language}] ${provider.label} \xB7 ${server}${subtitleLabel}`, - url: stream.url, - quality: getQuality(stream), - headers: { + return __async(this, null, function* () { + const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; + const language = variant === "dub" ? "DUB" : "SUB"; + return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map((stream) => __async(this, null, function* () { + const streamHeaders = stream.headers && typeof stream.headers === "object" ? stream.headers : {}; + const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL; + const subtitleLabel = subtitleCount > 0 && variant !== "dub" ? " \xB7 CC" : ""; + const headers = __spreadProps(__spreadValues({}, streamHeaders), { "Referer": referer, "Origin": getOrigin(referer), "User-Agent": HEADERS["User-Agent"] - } - }; + }); + const quality = yield resolveQuality(stream, headers); + const server = !stream.server || /^(?:HD|auto)$/i.test(stream.server) ? quality : stream.server; + return { + // Nuvio's source picker prominently displays `name`, so include the + // Miruro server and audio variant here instead of only in `title`. + name: `Miruro \xB7 ${provider.label} \xB7 ${language}`, + title: `[${language}] ${provider.label} \xB7 ${server}${subtitleLabel}`, + url: stream.url, + quality, + headers + }; + }))); }); } function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) { diff --git a/src/miruro/extractor.js b/src/miruro/extractor.js index 17683b2..d68d75d 100644 --- a/src/miruro/extractor.js +++ b/src/miruro/extractor.js @@ -2,10 +2,11 @@ * Resolve TMDB media to AniList, then retrieve Miruro episode sources. */ -import { BASE_URL, fetchJson, HEADERS, secureGet } from './http.js'; +import { BASE_URL, fetchJson, fetchText, HEADERS, secureGet } from './http.js'; const ANIMAP_URL = 'https://animap.id/api/map'; const ANILIST_URL = 'https://graphql.anilist.co'; +const HLS_QUALITY_TIMEOUT_MS = 4000; // Miruro's visible native-player providers and their preferred subtitle mode. const PROVIDERS = [ @@ -270,15 +271,71 @@ function getOrigin(url) { return match ? match[1] : BASE_URL; } -function getQuality(stream) { - const value = String(stream.quality || stream.server || 'HD'); +function getDeclaredQuality(stream) { + const value = `${stream.quality || ''} ${stream.resolution || ''} ${stream.server || ''}`; const resolution = value.match(/(2160|1440|1080|720|480|360)p?/i); if (resolution) { return `${resolution[1]}p`; } - return /4k/i.test(value) ? '4K' : 'HD'; + return /4k/i.test(value) ? '4K' : null; +} + +function getHlsQuality(manifest) { + const heights = []; + const pattern = /\bRESOLUTION\s*=\s*\d+\s*x\s*(\d+)\b/gi; + let match; + + while ((match = pattern.exec(manifest)) !== null) { + const height = Number(match[1]); + + if (height > 0) { + heights.push(height); + } + } + + return heights.length > 0 ? `${Math.max(...heights)}p` : null; +} + +function isHlsStream(stream) { + const type = String(stream && stream.type || '').toLowerCase(); + return type === 'hls' || type === 'm3u8' || type === 'application/x-mpegurl'; +} + +async function resolveQuality(stream, headers) { + const declaredQuality = getDeclaredQuality(stream); + + if (declaredQuality) { + return declaredQuality; + } + + if (isHlsStream(stream)) { + let timeoutId; + + try { + const manifest = await Promise.race([ + fetchText(stream.url, { headers }), + new Promise((resolve) => { + timeoutId = setTimeout(() => resolve(null), HLS_QUALITY_TIMEOUT_MS); + }) + ]); + const hlsQuality = typeof manifest === 'string' ? getHlsQuality(manifest) : null; + + if (hlsQuality) { + return hlsQuality; + } + } catch (_) { + // A stream can still be playable when its manifest rejects this + // metadata request, so retain it with an unknown HD quality. + } finally { + if (timeoutId != null) { + clearTimeout(timeoutId); + } + } + } + + return 'HD'; } function isPlayableStream(stream) { @@ -291,14 +348,26 @@ function isPlayableStream(stream) { ); } -function normalizeStreams(sourcePayload, provider, variant) { +async function normalizeStreams(sourcePayload, provider, variant) { const subtitleCount = Array.isArray(sourcePayload.subtitles) ? sourcePayload.subtitles.length : 0; const language = variant === 'dub' ? 'DUB' : 'SUB'; - return (sourcePayload.streams || []).filter(isPlayableStream).map((stream) => { - const referer = stream.referer || BASE_URL; - const server = stream.server || getQuality(stream); + return Promise.all((sourcePayload.streams || []).filter(isPlayableStream).map(async (stream) => { + const streamHeaders = stream.headers && typeof stream.headers === 'object' + ? stream.headers + : {}; + const referer = stream.referer || streamHeaders.Referer || streamHeaders.referer || BASE_URL; const subtitleLabel = subtitleCount > 0 && variant !== 'dub' ? ' · CC' : ''; + const headers = { + ...streamHeaders, + 'Referer': referer, + 'Origin': getOrigin(referer), + 'User-Agent': HEADERS['User-Agent'] + }; + const quality = await resolveQuality(stream, headers); + const server = !stream.server || /^(?:HD|auto)$/i.test(stream.server) + ? quality + : stream.server; return { // Nuvio's source picker prominently displays `name`, so include the @@ -306,14 +375,10 @@ function normalizeStreams(sourcePayload, provider, variant) { name: `Miruro · ${provider.label} · ${language}`, title: `[${language}] ${provider.label} · ${server}${subtitleLabel}`, url: stream.url, - quality: getQuality(stream), - headers: { - 'Referer': referer, - 'Origin': getOrigin(referer), - 'User-Agent': HEADERS['User-Agent'] - } + quality, + headers }; - }); + })); } async function getProviderStreams(provider, providerPayload, anilistId, episodeNumber) {