# Stremio Stream Formatter Patterns

Complete implementation patterns for Stremio stream `name` and `description` formatting, extracted from the ultimate-scraper v5.0 upgrade. These match and exceed AIOStreams' formatter quality.

## Resolution Badge Map

Maps quality strings to fixed-width badges for visual alignment in the stream list:

```js
const RESOLUTION_BADGE = {
  '4K':     '   4K   ',
  '2160p':  '   4K   ',
  '1440p':  '    2K   ',
  '1080p':  ' 1080P ',
  '720p':   '  720P  ',
  '480p':   '  480P  ',
  'SD':     '   SD   ',
  'CAM':    '  CAM  ',
};
```

## Quality Tag Map

Maps quality source strings to display tags wrapped in `〈⟩`:

```js
const QUALITY_LABEL = {
  'BluRay REMUX': 'Remux',
  'BluRay':        'BluRay',
  'WEB-DL':        'Web-DL',
  'WEBRip':        'WEBRip',
  'HDRip':         'HDRip',
  'HDTV':          'HDTV',
};
```

Implementation: iterate QUALITY_LABEL entries, test `quality.toLowerCase().includes(key.toLowerCase())`, return `〈${label}〉` on first match.

## Type Markers

Unicode superscript markers shown after resolution badge:

```js
const TYPE_MARKER = {
  p2p:     '⁽ᵖ²ᵖ⁾',
  debrid:  '    ',       // blank — debrid is the default, no marker needed
  http:    '⁽ʷᵉᵇ⁾',
  usenet:  '⁽ⁿᶻᵇ⁾',
  live:    '⁽ˡᶦᵛᵉ⁾',
  embed:   '⁽ᵉᵐᵇᵉᵈ⁾',
};
```

## Cache Indicator

```js
function formatCacheBadge(cached) {
  if (cached === true) return '⚡';
  if (cached === false) return '⏳';
  return '';
}
// cached = stream.cachedProviders?.length > 0
```

## Size Formatting

```js
function formatSize(bytes) {
  if (!bytes || bytes <= 0) return '';
  if (bytes >= 1073741824) return `${(bytes / 1073741824).toFixed(2)} GB`;
  if (bytes >= 1048576)    return `${(bytes / 1048576).toFixed(1)} MB`;
  return `${(bytes / 1024).toFixed(0)} KB`;
}
```

## Bitrate Formatting

```js
function formatBitrate(bps) {
  if (!bps || bps <= 0) return '';
  if (bps >= 1000000)
    return `${(bps / 1000000).toFixed(1)} Mbps`.replace('Mbps', 'ᴹᵇᵖˢ');
  return `${(bps / 1000).toFixed(0)} Kbps`.replace('Kbps', 'ᴷᵇᵖˢ');
}
```

## HDR Tag Detection

```js
function formatHDR(parsed) {
  const tags = [];
  if (parsed.isDV) tags.push('DV');
  if (parsed.isHDR) {
    if (parsed.hdrType === 'HDR10+') tags.push('HDR10+');
    else if (parsed.hdrType === 'HDR10') tags.push('HDR10');
    else tags.push('HDR');
  }
  if (parsed.isIMAX) tags.push('IMAX');
  if (tags.length === 0) return '';
  return `✦ ${tags.join(' · ')} `;
}
```

**Requires**: `releaseParser.js` to extract `hdrType` (HDR10+ vs HDR10 vs HDR) and `isIMAX` from release names.

## Audio Formatting

```js
function formatAudio(parsed) {
  const tags = [];
  if (parsed.audio?.length) tags.push(...parsed.audio);
  if (parsed.audioChannels?.length) tags.push(...parsed.audioChannels);
  if (tags.length === 0) return '';
  return `♬ ${tags.join(' · ')} `;
}
```

## Full Stream Name Builder

```js
function buildStreamName(stream, parsed, cached) {
  const parts = [];
  const resolution = stream.quality || parsed?.quality;
  parts.push(RESOLUTION_BADGE[resolution] || '      ');
  const type = stream.type || 'debrid';
  parts.push(TYPE_MARKER[type] || '');
  const cacheBadge = formatCacheBadge(cached);
  if (cacheBadge) parts.push(cacheBadge);
  const qualityTag = formatQualityTag(stream.quality || parsed?.quality);
  if (qualityTag) parts.push(qualityTag);
  return parts.join('').replace(/\s+/g, ' ').trim();
}
```

## Full Stream Description Builder

```js
function buildStreamDescription(stream, parsed, cached, context = {}) {
  const lines = [];

  // Line 1: Title + Year + S/E
  const titleParts = [];
  if (context.title) titleParts.push(`✎ ${context.title}`);
  if (context.year) titleParts.push(`(${context.year})`);
  if (context.season != null || context.episode != null) {
    const se = [];
    if (context.season != null) se.push(`S${context.season}`);
    if (context.episode != null) se.push(`E${context.episode}`);
    if (se.length) titleParts.push(se.join(' '));
  }
  if (titleParts.length) lines.push(titleParts.join(' '));

  // Line 2: Codec + HDR + Edition
  const techParts = [];
  if (parsed?.codec) techParts.push(`▣ ${parsed.codec}`);
  const hdr = formatHDR(parsed);
  if (hdr) techParts.push(hdr.trim());
  if (parsed?.edition) techParts.push(`[${parsed.edition}]`);
  if (techParts.length) lines.push(techParts.join(' • '));

  // Line 3: Audio
  const audio = formatAudio(parsed);
  if (audio) lines.push(audio);

  // Line 4: Size + Seeders + Age
  const metaParts = [];
  if (stream.size) metaParts.push(formatSize(stream.size));
  if (stream.seeders > 0) metaParts.push(`⇄ ${stream.seeders}❦`);
  if (stream.age) metaParts.push(`· ${stream.age}`);
  if (metaParts.length) lines.push(metaParts.join(' • '));

  // Line 5: Languages + Subtitles
  if (parsed?.languages?.length)
    lines.push(`⛿ ${parsed.languages.slice(0,4).join(' · ')}`);
  if (parsed?.subtitles?.length)
    lines.push(`(${parsed.subtitles.slice(0,4).join(' · ')})`);

  // Line 6: Source + Group + Score
  const srcParts = [];
  if (stream.source) srcParts.push(`[${stream.source}]`);
  if (parsed?.releaseGroup) srcParts.push(parsed.releaseGroup);
  if (stream.finalScore != null) srcParts.push(`[${stream.finalScore}]`);
  if (srcParts.length) lines.push(srcParts.join(' · '));

  return lines.filter(Boolean).join('\n') || stream.title || 'Unknown';
}
```

## Integration into Stremio Route

```js
router.get('/stream/:type/:id.json', async (req, res) => {
  const context = await buildContext(type, id);
  const results = await scrapeAll(context);
  const external = await fetchExternalStreams(type, id);
  let allResults = [...results, ...external];
  allResults = applySizeLimits(allResults);
  allResults = deduplicate(allResults);
  const ranked = rankResults(allResults, context);

  const torrentStreams = ranked.slice(0, 50).map(r => {
    const formatted = formatStream(r, context);
    return {
      name: formatted.name,
      title: r.title || context.title,
      description: formatted.description,
      infoHash: r.infoHash,
      fileIdx: r.fileIdx,
      quality: r.quality,
      size: r.size,
      seeders: r.seeders,
      cachedProviders: r.cachedProviders,
      finalScore: r.finalScore,
      source: r.source,
    };
  });

  const embedStreams = buildEmbedStreams(context);
  res.json({ streams: [...embedStreams, ...torrentStreams] });
});
```

## Release Parser Extensions

To support the HDR type and audio channels fields above, extend `releaseParser.js` with:

```js
const HDR_TAGS = [
  { pattern: /\bHDR10Plus\b/i, type: 'HDR10+' },
  { pattern: /\bHDR10\b/i,     type: 'HDR10' },
  { pattern: /\bHDR\b/i,       type: 'HDR' },
  { pattern: /\bHLG\b/i,       type: 'HLG' },
];
const IMAX_TAGS = [/\bIMAX\b/i];
const AUDIO_CHANNELS_RE = [
  { pattern: /\b(7\.1(?:[ .-]?Atmos)?)\b/i, channels: '7.1' },
  { pattern: /\b(5\.1(?:[ .-]?[46])?)\b/i,  channels: '5.1' },
  { pattern: /\b2\.0\b/i,                   channels: '2.0' },
];
```

Add fields: `hdrType`, `isIMAX`, `audioChannels` to the parsed result object.
