> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vyla.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# TV Episode Sources

> Stream verified HLS sources, subtitle tracks, and TMDB metadata for a TV episode via Server-Sent Events.

Identical to the movie endpoint in behavior, but requires a series ID plus season and episode numbers. Results stream via Server-Sent Events — the `meta` event fires immediately with subtitles and TMDB data, then each working provider emits its own `source` event as it resolves.

<Warning>
  This endpoint requires a `standard` or `partner` API key, or a session token from `POST /api/auth`. The `public` key will receive a `403` response.
</Warning>

***

## Query Parameters

<ParamField query="id" type="string" required>
  TMDB **series** ID — not an episode ID. Find it on [themoviedb.org](https://www.themoviedb.org) in the URL of the show's main page.

  **Example:** `themoviedb.org/tv/1396` → `id=1396` (Breaking Bad)
</ParamField>

<ParamField query="season" type="number" required>
  Season number. Use `1` for the first season.
</ParamField>

<ParamField query="episode" type="number" required>
  Episode number within the season. Use `1` for the first episode.
</ParamField>

<ParamField query="sources" type="string">
  Comma-separated list of provider keys to query. When omitted, all active providers are queried.

  **Example:** `sources=vidlink,vixsrc`

  Use `GET /api?sources_meta=1` to retrieve the full list of available provider keys.
</ParamField>

***

## Request

<CodeGroup>
  ```bash cURL (API key) theme={null}
  curl -N "https://1c34-y.hf.space/tv?id=1396&season=1&episode=1" \
    --header "Authorization: Bearer YOUR_API_KEY"
  ```

  ```bash cURL (session token) theme={null}
  TOKEN=$(curl -s -X POST https://1c34-y.hf.space/api/auth | jq -r .token)

  curl -N "https://1c34-y.hf.space/tv?id=1396&season=1&episode=1" \
    -H "X-Session-Token: $TOKEN"
  ```

  ```javascript JavaScript (session token) theme={null}
  const BASE = 'https://1c34-y.hf.space';

  const { token } = await fetch(`${BASE}/api/auth`, { method: 'POST' }).then(r => r.json());

  const res = await fetch(`${BASE}/tv?id=1396&season=1&episode=1`, {
    headers: { 'X-Session-Token': token }
  });
  const reader = res.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop();

    for (const line of lines) {
      if (!line.startsWith('data: ')) continue;
      const event = JSON.parse(line.slice(6));

      if (event.type === 'meta')   handleMeta(event);
      if (event.type === 'source') handleSource(event.source);
      if (event.type === 'done')   console.log(`Done. ${event.total} sources.`);
    }
  }
  ```

  ```typescript TypeScript (session token) theme={null}
  const BASE = 'https://1c34-y.hf.space';

  interface Source {
    source: string;
    label: string;
    url: string;
  }

  interface Subtitle {
    label: string;
    file: string;
    type: string;
    source: string;
  }

  interface MetaEvent   { type: 'meta';   meta: Record<string, unknown> | null; subtitles: Subtitle[]; }
  interface SourceEvent { type: 'source'; source: Source; }
  interface DoneEvent   { type: 'done';   total: number; }

  type SSEEvent = MetaEvent | SourceEvent | DoneEvent;

  async function streamEpisode(
    seriesId: number,
    season: number,
    episode: number,
    onSource: (s: Source) => void,
    onMeta?: (meta: MetaEvent) => void
  ) {
    const { token } = await fetch(`${BASE}/api/auth`, { method: 'POST' }).then(r => r.json());

    const res = await fetch(`${BASE}/tv?id=${seriesId}&season=${season}&episode=${episode}`, {
      headers: { 'X-Session-Token': token }
    });
    const reader = res.body!.getReader();
    const decoder = new TextDecoder();
    let buffer = '';

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      buffer += decoder.decode(value, { stream: true });
      const lines = buffer.split('\n');
      buffer = lines.pop()!;

      for (const line of lines) {
        if (!line.startsWith('data: ')) continue;
        const event: SSEEvent = JSON.parse(line.slice(6));
        if (event.type === 'meta')   onMeta?.(event);
        if (event.type === 'source') onSource(event.source);
      }
    }
  }
  ```

  ```python Python (server-side key) theme={null}
  import requests, json

  BASE = 'https://1c34-y.hf.space'

  with requests.get(
    f'{BASE}/tv',
    params={'id': 1396, 'season': 1, 'episode': 1},
    headers={'Authorization': 'Bearer your_standard_api_key'},
    stream=True
  ) as res:
      for line in res.iter_lines():
          if not line or not line.startswith(b'data: '):
              continue
          event = json.loads(line[6:])

          if event['type'] == 'meta':
              print('Episode:', event['meta'].get('name'))

          elif event['type'] == 'source':
              s = event['source']
              print(f"{s['label']}: {s['url']}")

          elif event['type'] == 'done':
              print(f"Done. {event['total']} sources.")
              break
  ```

  ```swift Swift theme={null}
  let url = URL(string: "https://1c34-y.hf.space/tv?id=1396&season=1&episode=1")!
  var request = URLRequest(url: url)
  request.setValue("your_session_token", forHTTPHeaderField: "X-Session-Token")

  class SSEDelegate: NSObject, URLSessionDataDelegate {
      func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
          guard let text = String(data: data, encoding: .utf8) else { return }
          for line in text.components(separatedBy: "\n") {
              guard line.hasPrefix("data: "),
                    let json = line.dropFirst(6).data(using: .utf8),
                    let event = try? JSONSerialization.jsonObject(with: json) as? [String: Any]
              else { continue }
              print(event)
          }
      }
  }

  let delegate = SSEDelegate()
  let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
  session.dataTask(with: request).resume()
  ```
</CodeGroup>

***

## SSE Event Reference

### `meta`

Emitted first, before any provider resolves.

<ResponseField name="type" type="string" required>
  Always `"meta"`.
</ResponseField>

<ResponseField name="meta" type="object | null">
  TMDB **episode-level** metadata — name, overview, air date, episode number, season number. `null` if no `TMDB_API_KEY` is configured.
</ResponseField>

<ResponseField name="subtitles" type="Subtitle[]" required>
  Available subtitle tracks for this specific episode. Empty array `[]` if none are found.

  <Expandable title="Subtitle object">
    <ResponseField name="label" type="string">
      Language name, e.g. `English`, `Spanish`.
    </ResponseField>

    <ResponseField name="file" type="string">
      Direct URL to a `.vtt` or `.srt` subtitle file.
    </ResponseField>

    <ResponseField name="type" type="string">
      Subtitle format — `vtt` or `srt`.
    </ResponseField>

    <ResponseField name="source" type="string">
      Internal subtitle source identifier.
    </ResponseField>
  </Expandable>
</ResponseField>

***

### `source`

Emitted once per verified, working provider.

<ResponseField name="type" type="string" required>
  Always `"source"`.
</ResponseField>

<ResponseField name="source" type="Source" required>
  <Expandable title="Source object">
    <ResponseField name="source" type="string">
      Internal provider key.
    </ResponseField>

    <ResponseField name="label" type="string">
      Human-readable provider name.
    </ResponseField>

    <ResponseField name="url" type="string">
      Fully-qualified, proxied stream URL. For HLS sources, pass to `hls.loadSource()` — all M3U8 segment and key URIs are rewritten to route through the proxy. For MP4 sources, set as `video.src` directly. No base URL prepending needed.
    </ResponseField>
  </Expandable>
</ResponseField>

***

### `done`

<ResponseField name="type" type="string" required>
  Always `"done"`.
</ResponseField>

<ResponseField name="total" type="number" required>
  Total number of working `source` events emitted.
</ResponseField>

***

## Status Codes

| Status | Meaning                                                |
| ------ | ------------------------------------------------------ |
| `200`  | SSE stream opened successfully                         |
| `400`  | Missing `id`, `season`, or `episode` parameter         |
| `401`  | Missing or invalid authentication                      |
| `403`  | Key tier does not have streaming access (`public` key) |
| `500`  | Server error before the stream could begin             |

<ResponseExample>
  ```text 200 theme={null}
  data: {"type":"meta","meta":{"id":1396,"name":"Pilot","season_number":1,"episode_number":1,"air_date":"2008-01-20"},"subtitles":[{"label":"English","file":"https://sub.vdrk.site/v1/tv/1396/1/1/English.vtt","type":"vtt","source":"v1"}]}

  data: {"type":"source","source":{"source":"vidlink","label":"VidLink","url":"https://1c34-y.hf.space/api?url=https%3A%2F%2F...&vl=1"}}

  data: {"type":"source","source":{"source":"meowtv","label":"MeowTV","url":"https://1c34-y.hf.space/api?url=https%3A%2F%2F...&mt=1"}}

  data: {"type":"done","total":2}
  ```

  ```json 400 theme={null}
  { "error": "missing parameters", "route": "/tv?id=:id&season=:s&episode=:e", "example": "/tv?id=1396&season=1&episode=1" }
  ```

  ```json 403 theme={null}
  { "error": "Public keys cannot access streaming endpoints" }
  ```
</ResponseExample>

***

## Notes

<AccordionGroup>
  <Accordion title="Filtering providers with sources=">
    Pass a comma-separated list of provider keys to query only specific providers:

    ```
    /tv?id=1396&season=1&episode=1&sources=vidlink,vixsrc
    ```

    Keys that don't match any active provider are silently ignored. If none of the requested keys match, the response will emit `done` with `total: 0`. Omit the parameter entirely to query all active providers.
  </Accordion>

  <Accordion title="Series ID vs Episode ID">
    The `id` parameter is the **series** TMDB ID — the same value regardless of which season or episode you request. Season and episode numbers are passed separately.

    | Wrong                      | Right                           |
    | -------------------------- | ------------------------------- |
    | Episode-level TMDB ID      | Series-level TMDB ID            |
    | `id=62085` (Pilot episode) | `id=1396` (Breaking Bad series) |
  </Accordion>

  <Accordion title="Specials and bonus content">
    Season `0` typically contains specials on TMDB. Support varies by provider — expect fewer working sources for specials.
  </Accordion>

  <Accordion title="Meta field is episode-level">
    Unlike the movie endpoint where `meta` describes the film, here `meta` contains **episode** data from TMDB — title, overview, air date, and episode number. Series-level metadata is not included.
  </Accordion>

  <Accordion title="Zero sources">
    If all providers fail, you'll receive a `done` event with `total: 0` and no `source` events. Check `/api/health` to see which providers are up.
  </Accordion>

  <Accordion title="Using EventSource vs fetch streaming">
    `EventSource` does not support custom headers, so you cannot send `X-Session-Token` through it. Use the `fetch` + `ReadableStream` approach shown in the examples above when authenticating with a session token.
  </Accordion>
</AccordionGroup>
