Skip to content

Commit 1760455

Browse files
committed
purstream: feed explicit facts into identity and shared Core display
1 parent d7ba2f4 commit 1760455

1 file changed

Lines changed: 29 additions & 46 deletions

File tree

Lines changed: 29 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,35 @@
1-
"""Reject Purstream episodic HLS whose real duration contradicts its metadata.
1+
#!/usr/bin/env python3
2+
"""Compose Purstream factual metadata projection with TV identity validation.
23
3-
Purstream can return a technically valid HLS with the requested title/episode in
4-
its display metadata while the media itself belongs to another work. For TV,
5-
series and anime requests, rows that advertise a duration are therefore checked
6-
against the media playlist #EXTINF total. A large mismatch is fail-closed.
7-
Movies and rows without an advertised duration are left unchanged.
4+
Purstream's provider adapter first exposes language/quality/codec/audio/duration
5+
as explicit stream facts. The existing episodic duration-identity guard then
6+
validates those rows. Final user-facing formatting remains entirely owned by
7+
the repository-wide Core presentation layer.
88
"""
99
from __future__ import annotations
1010

11-
import hashlib
12-
import json
11+
import importlib.util
12+
from pathlib import Path
1313
from typing import Any
1414

15-
MARKER = "NUVIO_PURSTREAM_TV_IDENTITY_V3"
16-
17-
18-
def apply(source: str, options: dict[str, Any] | None = None, **_kwargs: Any) -> str:
19-
cfg = dict(options or {})
20-
tolerance = max(0.15, min(float(cfg.get("duration_tolerance", 0.35)), 0.50))
21-
timeout_ms = max(2000, min(int(cfg.get("timeout_ms", 7000)), 12000))
22-
max_probes = max(1, min(int(cfg.get("max_probes", 3)), 5))
23-
payload = {
24-
"durationTolerance": tolerance,
25-
"timeoutMs": timeout_ms,
26-
"maxProbes": max_probes,
27-
}
28-
serialized = json.dumps(payload, separators=(",", ":"))
29-
marker = f"{MARKER}:{hashlib.sha256(serialized.encode()).hexdigest()[:12]}"
30-
if marker in source:
31-
return source
32-
33-
shim = r'''
34-
/* MARKER_PLACEHOLDER */
35-
;(function(g,c){"use strict";
36-
function txt(v){return String(v==null?"":v)}
37-
function req(a){var o=a[0]&&typeof a[0]==="object"?a[0]:{};return {type:txt(o.mediaType||o.type||a[1]||"movie").toLowerCase(),season:Number(o.season??a[2]??0)||0,episode:Number(o.episode??a[3]??0)||0}}
38-
function episodic(t){return t==="tv"||t==="series"||t==="anime"}
39-
function expected(row){var blob=[row&&row.name,row&&row.title,row&&row.size,row&&row.description].map(txt).join(" "),re=/\b(\d{1,3})\s*min(?:ute)?s?\b/ig,m,best=0;while((m=re.exec(blob))!==null)best=Math.max(best,Number(m[1])||0);return best?best*60:0}
40-
function hls(row){return /\.m3u8(?:[?#]|$)/i.test(txt(row&&row.url))||txt(row&&row.type).toLowerCase()==="hls"||txt(row&&row.format).toLowerCase()==="m3u8"}
41-
function headers(row){var h={};try{Object.assign(h,row&&row.headers||{},row&&row.behaviorHints&&row.behaviorHints.proxyHeaders&&row.behaviorHints.proxyHeaders.request||{})}catch(_e){}if(!h.Accept)h.Accept="*/*";if(!h["User-Agent"])h["User-Agent"]="Mozilla/5.0 (Linux; Android 14; Android TV) NuvioTV";return h}
42-
async function get(url,h){var ctrl=typeof AbortController!=="undefined"?new AbortController():null,timer=null;try{if(ctrl)timer=setTimeout(function(){try{ctrl.abort()}catch(_e){}},c.timeoutMs);var r=await g.fetch(url,{method:"GET",headers:h,redirect:"follow",signal:ctrl?ctrl.signal:void 0});if(!r||!r.ok)return null;var body=await r.text();return {body:txt(body).replace(/^\uFEFF/,"").trimStart(),url:txt(r.url||url)}}catch(_e){return null}finally{if(timer)clearTimeout(timer)}}
43-
function child(master,base){var lines=txt(master).split(/\r?\n/);for(var i=0;i<lines.length;i++){if(!/^#EXT-X-STREAM-INF:/i.test(lines[i].trim()))continue;for(var j=i+1;j<lines.length;j++){var v=lines[j].trim();if(!v)continue;if(v[0]==="#")continue;try{return new URL(v,base).toString()}catch(_e){return ""}}}return ""}
44-
function duration(media){var total=0,count=0,re=/^#EXTINF:([0-9.]+)/i;txt(media).split(/\r?\n/).forEach(function(line){var m=re.exec(line.trim());if(m){total+=Number(m[1])||0;count++}});return count?total:0}
45-
async function prove(row,seconds){var first=await get(txt(row.url),headers(row));if(!first||!/^#EXTM3U/i.test(first.body))return false;var media=first,variant=child(first.body,first.url);if(variant){media=await get(variant,headers(row));if(!media||!/^#EXTM3U/i.test(media.body))return false}var actual=duration(media.body);if(!actual)return false;var ratio=actual/seconds;return ratio>=(1-c.durationTolerance)&&ratio<=(1+c.durationTolerance)}
46-
function rows(v){if(Array.isArray(v))return {key:null,list:v};if(v&&typeof v==="object"){for(var i=0;i<3;i++){var k=["streams","results","data"][i];if(Array.isArray(v[k]))return {key:k,list:v[k]}}}return null}
47-
function rebuild(v,slot,list){if(slot.key===null)return list;var out=Object.assign({},v);out[slot.key]=list;return out}
48-
function install(o,k){if(!o||typeof o[k]!=="function"||o[k].__nuvioPurstreamTvIdentityV3)return false;var native=o[k];var wrap=async function(){var r=req(arguments),v=await native.apply(this,arguments);if(!episodic(r.type))return v;var slot=rows(v);if(!slot)return v;var kept=[],probes=0;for(var i=0;i<slot.list.length;i++){var row=slot.list[i],seconds=expected(row);if(seconds&&hls(row)){if(probes>=c.maxProbes)continue;probes++;if(await prove(row,seconds))kept.push(row);continue}kept.push(row)}return rebuild(v,slot,kept)};wrap.__nuvioPurstreamTvIdentityV3=true;wrap.__nuvioPurstreamTvIdentityOriginal=native;o[k]=wrap;return true}
49-
var ok=false;try{if(typeof module!=="undefined"&&module.exports)ok=install(module.exports,"getStreams")}catch(_e){}try{if(g&&typeof g.getStreams==="function"){if(ok&&typeof module!=="undefined"&&module.exports)g.getStreams=module.exports.getStreams;else install(g,"getStreams")}}catch(_e){}
50-
})(typeof globalThis!=="undefined"?globalThis:this,CONFIG_PLACEHOLDER);
51-
'''.replace("MARKER_PLACEHOLDER", marker).replace("CONFIG_PLACEHOLDER", serialized)
52-
return source.rstrip() + "\n" + shim.lstrip()
15+
HERE = Path(__file__).resolve().parent
16+
17+
18+
def _load(filename: str, module_name: str):
19+
path = HERE / filename
20+
spec = importlib.util.spec_from_file_location(module_name, path)
21+
if spec is None or spec.loader is None:
22+
raise RuntimeError(f"cannot load Purstream layer: {path}")
23+
module = importlib.util.module_from_spec(spec)
24+
spec.loader.exec_module(module)
25+
return module
26+
27+
28+
_FACTS = _load("purstream_stream_facts_v1.py", "nuvio_purstream_stream_facts_v1")
29+
_IDENTITY = _load("purstream_tv_identity_impl_v3.py", "nuvio_purstream_tv_identity_impl_v3")
30+
MARKER = _IDENTITY.MARKER
31+
32+
33+
def apply(source: str, options: dict[str, Any] | None = None, **kwargs: Any) -> str:
34+
with_facts = _FACTS.apply(source, options=options, **kwargs)
35+
return _IDENTITY.apply(with_facts, options=options, **kwargs)

0 commit comments

Comments
 (0)