# My TV Channel — Agent Guide

You are operating a **My TV Channel** account on behalf of its owner. This service
runs 24/7 linear HLS TV channels: the owner uploads videos, the service transcodes
them on an encoding farm and loops them into a continuous schedule, and viewers
watch at `https://{handle}.my-tv-channel.com` or in the My TV Channel apps
(iOS, tvOS, macOS, Android, Fire TV, Roku).

## Setup

**If your client speaks MCP, use that instead of raw HTTP** — the tools are already described
for you and you can skip this document:

```
https://my-tv-channel.com/api/mytvchannel/mcp    (Streamable HTTP, Bearer mytv_sk_...)
```

Setup and client config: <https://my-tv-channel.com/api/mytvchannel/mcp/README.md>

Otherwise, drive the REST API directly:

- **Base URL:** `https://my-tv-channel.com/api/mytvchannel`
- **Auth header (every call):** `Authorization: Bearer mytv_sk_...`
- The owner creates the key in the app (Profile → Agent Access) or at
  <https://my-tv-channel.com/agent-keys/>. If you don't have a key, ask them to do that.
- Responses are always `{"success": true, "data": {...}}` or
  `{"success": false, "error": "...", "code": N}`.
- Rate limit: 1,000 requests/hour per key → HTTP 429 with `Retry-After`.
- The key inherits the owner's subscription tier. The tier gate you may hit:
  manual scheduling (`schedule/batch-insert.php`) = **Pro+**. Public channel listing
  (`is_public`) is available on **every** tier, including Starter — never tell the owner
  to upgrade for it. Upload and streaming quotas are weekly and tier-based — check
  `users/status.php` before uploading; it reports the real limits for this account.

## Golden path: from nothing to a live channel

### 1. Orient yourself

```bash
curl -s -H "Authorization: Bearer $KEY" \
  https://my-tv-channel.com/api/mytvchannel/users/status.php
```

Returns the owner's tier and limits, upload/stream quota used vs remaining
(rolling 7 days), existing channels, and for each channel its `launchGaps`
(e.g. `"needs 2 more ready asset(s)"`). Always start here.

### 2. Create the channel

```bash
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"handle":"retro-arcade","name":"Retro Arcade TV","channel_type":"thematic",
       "description":"24/7 retro gaming","primary_language":"en","tags":["gaming","retro"]}' \
  https://my-tv-channel.com/api/mytvchannel/channels/create.php
```

- `handle`: 3–63 chars, lowercase `[a-z0-9-]`, becomes the URL. Check first with
  `GET /channels/check-handle.php?handle=retro-arcade`.
- `channel_type`: `thematic` or `location` (location requires a
  `location: {latitude, longitude, radius_km, name}` object).
- Channels start private. `is_public: true` lists the channel in Discover — allowed on
  every tier.

### 3. Add content

Upload files (MP4/MOV/M4V/MPEG/AVI):

```bash
curl -s -X POST -H "Authorization: Bearer $KEY" \
  -F "file=@video.mp4" -F "channel_id=42" -F "title=Episode 1" \
  https://my-tv-channel.com/api/mytvchannel/assets/upload.php
```

**Or import from a URL — usually what you want.** If the owner gave you a link rather
than a file, do not download it yourself:

```bash
curl -s -X POST -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"channel_id":42,"url":"https://example.com/episode1.mp4","title":"Episode 1","weight":7}' \
  https://my-tv-channel.com/api/mytvchannel/assets/import-url.php
```

The server fetches it. Requirements and limits:

- A **direct, public** media link — `mp4/mov/m4v/mpeg/mpg/avi`, or a **finite** `.m3u8`
  (VOD). Not a web page, not behind a login, not a live stream.
- Rejected immediately (400) if it cannot be probed as video, so you get a clear error
  rather than a silent failure later.
- Same weekly quota as uploads, charged on the video's duration. Checked at submit **and**
  re-verified against the real file after download — a source that misreports its duration
  fails with `quota_exceeded`.
- Max 20 queued imports at once (429 beyond that). Submitting the same URL to the same
  channel twice returns the original asset with `duplicate: true` rather than importing again.
- The asset appears instantly with `uploadStatus: "downloading"`, then follows the normal
  `pending → transcoding → ready` path. Poll `assets/status.php` exactly as for uploads.

**Or import the owner's own YouTube videos.** `POST /assets/import-youtube.php` with
`{"channel_id": 42, "url": "https://www.youtube.com/watch?v=..."}`.

This only works for videos the owner can **prove** they uploaded, and you cannot do that
step for them:

1. Get their tag from `GET /users/device.php` — it looks like
   `MyTVChannelApp#61030AF7-FC5C-4131-AA9F-1E62DC0BA25F`.
2. Ask them to paste that exact string into the video's **description or tags on YouTube**.
   Only the video's uploader can edit those, which is the proof.
3. Retry the import.

If the tag is missing you get a 403 whose message already contains the exact string to
paste — **relay it verbatim**, do not paraphrase it. Public finished videos only: no live
streams, no private or age-restricted videos, no playlists or channel URLs. Capped at 10
imports per day. Never try to import a video the owner did not upload — the check exists
because a channel built from other people's videos is a copyright problem for them.

Quota errors come back as HTTP 403 with the remaining allowance. Duration counts
against a weekly (rolling 7-day) upload quota.

### 4. Poll until ready

```bash
curl -s -H "Authorization: Bearer $KEY" \
  "https://my-tv-channel.com/api/mytvchannel/assets/status.php?channel_id=42"
```

Each asset reports `uploadStatus` (`pending → transcoding → ready`) and transcode
progress. The response `summary` tells you when `meetsLaunchRequirements` is true:
**≥ 4 ready assets AND ≥ 15 minutes total content**. Poll every 30–60 s;
transcoding takes roughly real-time ÷ 4 depending on farm load.
If a job fails, `POST /assets/retry.php` with `{"asset_id": N}`.

### 5. Shape the schedule

> ⚠️ **Weights only take effect when the schedule is (re)generated.**
> A channel auto-launches the moment it crosses 4 ready assets / 15 minutes — which happens
> *between* step 4 and step 5 — and its schedule is built from the weights that exist at that
> instant. Weights you set afterwards are stored correctly but do not air until a regeneration.
>
> Measured on a real run: the channel went live 16s after the final transcode; weights set 20s
> later had *zero* effect — a weight-3 asset got identical airtime to a weight-9 asset.
>
> **Two rules:**
> 1. Assign each asset its weight as soon as that asset reports `ready`, rather than batching
>    all the weight calls at the end. Then the weights are already in place at launch.
> 2. If you change weights on an **already-live** channel, you must call
>    `POST /schedule/regenerate.php` with `{"channel_id": N}` afterwards, or the change will
>    not air until the hourly cron. Throttled to one call per channel per 5 minutes — on 429,
>    honour `Retry-After`; do not spin.

Scheduling today is **weighted-random with sequence support**:

- **Weight** (0–10, default 5) controls how often an asset plays.
  `0` removes it from scheduling entirely.
  `PUT /assets/update.php` with `{"asset_id": N, "weight": 8}`.
  ⚠️ Unlike `channels/update.php?id=N`, the asset id goes in the **body** as
  `asset_id` (`?id=` is accepted too, but the body form is canonical).
- **Ordered mini-series:** tag assets `seqXX01`, `seqXX02`, … (XX = any two
  letters, e.g. `seqep01`) and they always play in order; only the `01`
  episode enters the random draw.
- **Manual placement** (Pro+): `POST /schedule/batch-insert.php` with
  `{"channelId": 42, "assetIds": [7,8,9], "mode": "end"}` (or
  `"mode": "at_time", "startTime": "..."`).

Translate the owner's intent into these primitives. Examples:
- "play the trailer often" → trailer weight 9
- "these 5 episodes in order" → tag them `seqep01`…`seqep05`
- "rarely show the old stuff" → weight 1–2

### Schedule rules — fixed slots and dayparts (Pro+)

For "news at 18:00" or "cartoons 06:00–09:00", weights are not enough. Rules are
**durable intent**: the scheduler re-honours them every time it rebuilds, so you compile
the owner's wishes once instead of babysitting the grid.

**Always validate first.** `POST /schedule/validate.php` takes exactly the payload
`rules.php` accepts, saves nothing, and returns `{valid, errors[], warnings[], preview[]}`
— including a simulated 24h grid. It is not tier-gated, so you can preview on any plan.
Then `PUT /schedule/rules.php` to commit (this **replaces the whole ruleset**).

```json
{
  "channel_id": 42,
  "timezone": "Europe/Paris",
  "rules": [
    {"rule_type": "time_slot", "start_time": "18:00", "days_mask": 127,
     "selector_type": "tag", "selector_value": "news", "priority": 10},
    {"rule_type": "time_slot", "start_time": "06:00", "end_time": "09:00",
     "days_mask": 31, "selector_type": "tag", "selector_value": "cartoons"},
    {"rule_type": "daypart_weight", "start_time": "22:00", "end_time": "06:00",
     "days_mask": 127, "selector_type": "tag", "selector_value": "kids",
     "weight_multiplier": 0}
  ]
}
```

- `days_mask` is a bitmask — bit0=Mon … bit6=Sun. `127`=every day, `31`=Mon–Fri, `96`=weekend.
- Times are **wall clock in the channel timezone**. Set `timezone` to an IANA name or slots
  mean UTC, which is almost never what the owner meant.
- `end_time` on a `time_slot` makes it a **block**: it keeps drawing from the selector until
  that time. Without it you get a single programme.
- `selector_type: "playlist"` with an array of asset ids plays them **in order**.
- `weight_multiplier: 0` removes matching assets from that window entirely.

**Translating what owners say:**

| They say | You write |
|---|---|
| "news at 6pm" | `time_slot` 18:00, days_mask 127 |
| "cartoons 6–9am on weekdays" | `time_slot` 06:00–09:00, days_mask 31 |
| "these five episodes in order" | `time_slot`, `selector_type: playlist` |
| "more music at night" | `daypart_weight` 22:00–06:00, multiplier 3 |
| "nothing for kids after 10pm" | `daypart_weight` 22:00–06:00, multiplier 0 |

> ⚠️ **Slots start ON-OR-AFTER their time.** The programme running into a slot is never cut
> short, so an 18:00 slot may actually begin at 18:02. That is deliberate — a linear channel
> must not chop a video in half to hit a clock time. Do not report it as a fault.

### 6. Go live

The channel launches **automatically** once launch requirements are met — the
scheduler builds a 7-day schedule within a minute or two. Confirm with
`GET /channels/get.php?id=42` → `schedule_status: "active"`, then report the
live URL to the owner: `https://retro-arcade.my-tv-channel.com`

The schedule regenerates automatically (weekly, and ~1 h after new content).

## Ongoing management

| Task | Call |
|---|---|
| Add more videos | `POST /assets/upload.php`, then poll status |
| Adjust rotation | `PUT /assets/update.php` body `{asset_id, weight, tags, title}` |
| Rename/describe channel | `PUT /channels/update.php?id=N` |
| Check quotas | `GET /users/status.php` or `GET /subscriptions/quota.php` |
| Apply weight changes now | `POST /schedule/regenerate.php` body `{channel_id}` (1 per channel per 5 min) |
| Remove a video | `POST /assets/delete.php` body `{asset_id}` — soft delete, auto-regenerates |
| Take a channel off air | `POST /channels/delete.php` body `{channel_id}` — archives, not destroys |
| Read the EPG | `GET /channels/schedule.php?channel_id=N` (no auth — `data` is an **array**, not an object) |
| List owner's channels | `GET /channels/list.php?owner=me` |

## Rules of engagement

- **Never ask the owner for their Apple password or app login** — the API key is
  all you need. Key management itself (create/revoke) is deliberately impossible
  with an API key.
- Respect quotas: check `users/status.php` before bulk uploads and tell the
  owner when a plan upgrade would be needed instead of retrying into 403s.
- On 429, honor `Retry-After`. On 5xx, back off and retry once or twice.
- Weights are the scheduling contract: don't set everything to 10 — differences
  between weights are what shape the channel. And they only air after a (re)generation.
- **Deletes are reversible by design, so prefer them to leaving debris.** `assets/delete.php`
  soft-deletes (the file survives a grace period) and `channels/delete.php` archives rather
  than destroys. If you created something by mistake, clean it up rather than leaving it for
  the owner. Both are idempotent — safe to retry.
- Never delete content you did not create in this session unless the owner asked you to.
