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

# Playlist

> How GlobalTV Roku fetches, parses, and delivers the M3U8 channel list to the rest of the app.

## Overview

After a successful authentication, `PlaylistTask` downloads a server-generated M3U8 playlist and passes the parsed channel and category data to global state. `MainScreen` then reads this data to render the channel grid.

<CardGroup cols={2}>
  <Card title="PlaylistTask" icon="bolt">
    Downloads the M3U8 playlist via HTTP and writes results to `m.global`
  </Card>

  <Card title="M3UParser" icon="list">
    Parses raw M3U8 text into structured channel and category arrays
  </Card>

  <Card title="m.global.channelList" icon="database">
    Global state field where the parsed channel array is stored
  </Card>

  <Card title="MainScreen" icon="grid-2">
    Consumes `m.global.channelList` and `m.global.categories` to render the channel grid
  </Card>
</CardGroup>

***

## Playlist endpoint

The playlist URL is built from `AppConstants.PATH_PLAYLIST_TPL`:

```brightscript theme={null}
PATH_PLAYLIST_TPL : "/auth/{user}/{pass}/playlist/m3u8/hls"
```

At runtime, `PlaylistTask` builds the URL with the current credentials URL-encoded:

```brightscript theme={null}
encUser = GTV_UrlEncode(username)
encPass = GTV_UrlEncode(password)
url = server + "/auth/" + encUser + "/" + encPass + "/playlist/m3u8/hls"
```

The endpoint both authenticates the request and returns the playlist in a single round-trip, eliminating the need for a separate session token.

***

## PlaylistTask fetch flow

<Steps>
  <Step title="Server selection">
    `PlaylistTask` reads `m.global.activeServer` (set by `AuthTask` during login). If that is empty, it calls `GTV_FindActiveServer()` to probe the server list again.
  </Step>

  <Step title="HTTP fetch with retry">
    The task fetches the playlist using `GTV_HttpGet` with up to `RETRY_MAX = 3` attempts. On network-level failures (`httpCode = -1`), it advances to the next server in the request sequence (`GTV_ServerBuildRequestSequence`). On non-network HTTP errors, it stops immediately:

    ```brightscript theme={null}
    c = AppConstants()
    while retries < c.RETRY_MAX
        resp = GTV_HttpGet(url, c.TIMEOUT_HTTP)
        if resp.ok then exit while
        if resp.code <> -1  ' non-network error — stop
            exit while
        end if
        ' advance to next server for network errors
        retries = retries + 1
    end while
    ```
  </Step>

  <Step title="Auth failure detection">
    If the HTTP response is not OK, `PlaylistTask` passes the response body through `GTV_AuthClassifyFailure` — the same classifier used by `AuthTask`. A `401`/`470`/`471` response triggers the appropriate credential or inactive error message.
  </Step>

  <Step title="Parsing">
    On a successful response, the task passes the raw body to `GTV_ParseM3U`:

    ```brightscript theme={null}
    parsed = GTV_ParseM3U(resp.body)
    ```

    The parser returns a result object with `channels`, `categories`, `droppedMissingNumber`, `droppedDuplicateNumber`, and `fallbackAssigned` counts.
  </Step>

  <Step title="Global state update">
    Valid results are written to global state:

    ```brightscript theme={null}
    m.global.channelList = parsed.channels
    m.global.categories  = parsed.categories
    ```

    `m.top.result` and `m.top.done` are then set, signalling `MainScene` to proceed to `MainScreen`.
  </Step>
</Steps>

***

## M3U8 format and parser

### What M3U8 looks like

The server returns a standard M3U8 extended playlist. Each channel entry is a pair of lines:

```text theme={null}
#EXTINF:-1 tvg-id="123" tvg-name="Canal Example" tvg-logo="https://..." group-title="Noticias" channel-number="12",Canal Example
https://stream.example.com/live/abc123def456/playlist.m3u8
```

### Parser: `GTV_ParseM3U`

`M3UParser.brs` implements `GTV_ParseM3U(text)`. For each `#EXTINF` line it:

1. Extracts `tvg-id`, `tvg-name`, `tvg-logo`, `group-title`, and `channel-number` attributes using `GTV_ExtractAttr`.
2. Falls back to the comma-separated display name if `tvg-name` is absent.
3. Reads the URL from the next non-comment, non-empty line.
4. Normalizes `hls://` and `hlss://` URL schemes to `http://` and `https://` respectively.
5. Attempts to extract a stream ID (hex string ≥ 12 characters) from the URL path segments and stores it in `streamId` / `contentId`.

### Strict channel number mode

`PARSER_STRICT_CHANNEL_NUMBER = true` is the production default. In strict mode:

* Channels with no valid `channel-number` attribute are **dropped** (counted in `droppedMissingNumber`).
* Channels with a duplicate number are **dropped** (counted in `droppedDuplicateNumber`).
* No auto-numbering fallback is applied.

```brightscript theme={null}
PARSER_STRICT_CHANNEL_NUMBER : true
```

<Warning>
  In strict mode, any channel entry in the M3U8 that lacks a positive integer `channel-number` attribute will be silently dropped. Ensure the backend always populates this field.
</Warning>

When `PARSER_STRICT_CHANNEL_NUMBER = false`, missing numbers are assigned sequentially starting from 1 (`fallbackAssigned` count increases).

### Channel object structure

Each item in the parsed `channels` array has the following fields:

| Field       | Source                    | Description                           |
| ----------- | ------------------------- | ------------------------------------- |
| `id`        | `tvg-id`                  | Provider channel ID                   |
| `name`      | `tvg-name` or comma field | Display name                          |
| `logo`      | `tvg-logo`                | Logo URL                              |
| `group`     | `group-title`             | Category name (defaults to `General`) |
| `number`    | `channel-number`          | Integer channel number                |
| `url`       | Next non-comment line     | Normalized HLS stream URL             |
| `streamId`  | Extracted from URL path   | Hex stream identifier (if detectable) |
| `contentId` | `streamId` or `number`    | Used for ad targeting                 |

### Channel sorting

After parsing, channels are sorted ascending by `number` using insertion sort (`GTV_SortChannels`).

### Category extraction

Every unique `group-title` value becomes an entry in the `categories` array. Grouping is case-insensitive (keyed by `LCase(ch.group)`).

***

## Global state fields

| Field                  | Type  | Set by         | Consumed by                                    |
| ---------------------- | ----- | -------------- | ---------------------------------------------- |
| `m.global.channelList` | Array | `PlaylistTask` | `PlayerScreen`, `MainScreen`, `AdsPollingTask` |
| `m.global.categories`  | Array | `PlaylistTask` | `MainScreen`                                   |

***

## How MainScreen consumes the channel list

`MainScreen` reads `m.global.channelList` and `m.global.categories` after `PlaylistTask` signals completion. It uses these arrays to populate the channel grid. When the user selects a channel, `MainScreen` passes the channel index to `PlayerScreen` via `m.global.currentChannelIndex`.

`PlayerScreen.init()` also reads `m.global.channelList` directly:

```brightscript theme={null}
sub init()
    m.channels = m.global.channelList
    ...
end sub
```

When `AdsPollingTask` needs to resolve the current stream position, it also reads from `m.global.channelList`:

```brightscript theme={null}
function GTV_GetCurrentStreamPosition() as String
    idx = m.global.currentChannelIndex
    channels = m.global.channelList
    if channels <> invalid and idx >= 0 and idx < channels.Count()
        ch = channels[idx]
        if ch <> invalid and ch.number <> invalid
            return GTV_AdsSafeTrim(ch.number.ToStr())
        end if
    end if
    return ""
end function
```

***

## Parse diagnostics

`PlaylistTask` logs a warning for each category of dropped or fallback-assigned channel:

| Event                              | Log message                                                   |
| ---------------------------------- | ------------------------------------------------------------- |
| Channel dropped — missing number   | `event=parser_drop reason=missing_channel_number count=N`     |
| Channel dropped — duplicate number | `event=parser_drop reason=duplicate_channel_number count=N`   |
| Fallback number assigned           | `event=parser_fallback reason=missing_channel_number count=N` |

These warnings appear as `GTV_Warn` entries and are always printed, regardless of build type.
