# Browser Compatibility Scoring for Debrid Streams

## Problem

The stream pipeline returns RD-cached media files for a given title. Not all files play in a browser `<video>` element. The first resolved stream may be a 55 GB UHD MKV remux with TrueHD Atmos — unplayable in any browser — while a 3 GB 1080p h.264 MP4 exists deeper in the candidate list but was never surfaced.

The scoring system ranks streams so the best playable option is surfaced first and auto-played.

## Scoring Function

File: `app/modules/streaming/presentation/routers.py`

```python
def _browser_score(result: StreamResolveResult, candidate: TorrentioCandidate | None = None) -> int:
    """Score 0-100 for browser <video> compatibility."""
    score = 50  # neutral baseline

    # --- Filename/URL heuristics (strongest signal) ---
    url_or_name = (result.stream_url + " " + (result.filename or "")).lower()

    # Containers
    if ".mp4" in url_or_name:
        score += 30
    elif ".webm" in url_or_name:
        score += 20
    elif ".mkv" in url_or_name or ".m2ts" in url_or_name:
        score -= 20
    elif url_or_name.endswith(".m3u8"):
        score += 10

    # Codecs
    if "h264" in url_or_name or "avc" in url_or_name or "x264" in url_or_name:
        score += 20
    if "h265" in url_or_name or "hevc" in url_or_name or "x265" in url_or_name:
        score -= 15
    if "av1" in url_or_name:
        score -= 10
    if "vp9" in url_or_name:
        score -= 5

    # Audio (TrueHD/Atmos/DTS-X likely high-bitrate mux that chokes browser)
    if "truehd" in url_or_name or "atmos" in url_or_name or "dts" in url_or_name:
        score -= 10
    if "flac" in url_or_name or "pcm" in url_or_name:
        score -= 5

    # HDR/DV
    if "hdr" in url_or_name or "dolby vision" in url_or_name or "dv " in url_or_name:
        score -= 15
    if "sdr" in url_or_name:
        score += 5

    # Size heuristic
    if result.size_bytes and result.size_bytes > 0:
        gb = result.size_bytes / (1024**3)
        if 0.3 <= gb <= 4:
            score += 15
        elif 4 < gb <= 10:
            score += 5
        elif gb > 30:
            score -= 20
        elif gb > 15:
            score -= 10

    # Quality
    q = (result.quality.value if result.quality else "").lower()
    if q == "sd":
        score += 5
    elif q == "hd":
        score += 10
    elif q == "fhd" or q == "1080p":
        score += 15
    elif q == "uhd" or q == "4k" or q == "2160p":
        score -= 10

    # --- Torrentio title signals ---
    if candidate and candidate.title:
        ct = candidate.title.lower()
        if "1080p" in ct:
            score += 10
        if "x265" in ct or "hevc" in ct:
            score -= 10
        if "x264" in ct or "avc" in ct:
            score += 15
        if "hdr" in ct or "dolby" in ct or "dv" in ct:
            score -= 10
        if "remux" in ct:
            score -= 15

    return max(0, min(100, score))
```

## Sorting

After scoring, sort results by score descending with larger-file-first tiebreaker (prefer smaller for same score):

```python
all_scored.sort(key=lambda x: (-x[0], -(x[1].size_bytes or 0)))
```

## Torrentio Metadata Preservation

To score accurately, preserve Torrentio metadata through the resolve pipeline:

```python
@dataclass
class TorrentioCandidate:
    info_hash: str
    title: str | None = None
    seeders: int | None = None
    idx: int = 0
```

Build candidates from Torrentio stream entries before resolving:

```python
candidates = []
for idx, ts in enumerate(torrent_streams):
    info_hash = ts.get("infoHash", "")
    if isinstance(info_hash, str) and re.match(r"^[0-9a-fA-F]{40}$", info_hash.strip()):
        title = ts.get("title") or None
        seeders = None
        if title:
            m = re.search(r'👤 (\d+)', title)
            if m:
                seeders = int(m.group(1))
        candidates.append(TorrentioCandidate(
            info_hash=info_hash.strip().lower(),
            title=title,
            seeders=seeders,
            idx=idx,
        ))
```

After each magnet resolves, match back to its candidate:

```python
all_scored = []
seen_urls = set()
for c in candidates[:50]:
    magnet = f"magnet:?xt=urn:btih:{c.info_hash}"
    try:
        results = await use_cases.resolve(user_id, req)
    except ...:
        continue
    for r in results:
        if r.stream_url not in seen_urls:
            seen_urls.add(r.stream_url)
            score = _browser_score(r, c)
            all_scored.append((score, r, c))
```

Return with metadata attached to frontend response:

```python
return ResolveSuccessResponse(streams=[
    _to_result_response(r, torrent_title=c.title, seeders=c.seeders)
    for score, r, c in all_scored
])
```

## Frontend Auto-Play

After resolving, auto-play the highest-scored (first) stream:

```typescript
try {
  const res = await resolveStreamByTMDB(tmdbId, mediaType, account.id);
  const sorted = res.streams ?? [];
  setStreams(sorted);

  if (sorted.length > 0 && !activeStream) {
    const best = sorted[0];
    addDevEvent("auto_play", `quality=${best.quality || "?"} provider=${best.provider}`);
    setTimeout(() => handlePlayStream(best), 100); // delay for UI update
  }
} catch (err) {
  // ...
}
```

The 100ms delay allows React to render the stream list before playback starts, preventing a jarring UX gap.

## When All Streams Score Low

If `best_score < 30` after sorting:
1. Try RD transcode API (`/streaming/transcode/{id}`) for HLS conversion
2. Backend FFmpeg transcoding proxy (expensive, only for critical titles)
3. Show inline warning: "This stream may not play in your browser — try a smaller file or use an external player"
