AI Prompt
Paste this into any AI assistant (a chat or a coding agent) to have it build a tool against the tvlogo.monster API for you. It describes the endpoints, keys, options, rate limits and attribution rules, then adds your options below and the task you pick. "Fix broken logos in my M3U playlist using tvlogo.monster pypi" or use the elaborate options below. 😊
# Build a tool for the tvlogo.monster TV channel logo API
You are an experienced software engineer. Write the tool described in the **Task** section at the end, using only the API documented below. Do not invent endpoints, parameters or fields; if something is unclear, ask me.
## How to work
- Ask me which programming language I want before writing code, unless I have already said so.
- If I choose **Python**, use the official `tvlogo.monster` client: `pip install tvlogo.monster` (PyPI normalises it to `tvlogo-monster`; the import name is `tvlogo`), then `from tvlogo import Client`, `client = Client()`, `logo = client.resolve(key, mode=..., theme=..., era=..., size=..., namespace=...)`, which returns `None` when there is no logo or an object with `image_url`, `resolved_level`, `fallback` and `theme_served` (it retries 429 after `Retry-After`). It is on PyPI (https://pypi.org/project/tvlogo.monster/). Version 0.1.0 has no station search call, so use plain `requests` for `/v1/stations` and `/v1/affiliates`.
- For any other language, call the REST API directly with the standard library or the de-facto HTTP library, and note dependencies.
- No API key or sign-up is needed. Send a descriptive `User-Agent` such as `my-m3u-fixer/1.0 (+https://example.org/contact)`.
- Production quality: timeouts on every request, clear error messages, a configurable base URL, and no busy loops.
## About tvlogo.monster
tvlogo.monster is a non-profit, open-source community project: a database, API and website of TV channel logos (Canada first), and a sister project of epg.monster. It maps the ids used in IPTV playlists and guides to clean, square PNG logos.
Channel logos are trademarks and/or copyright of their respective owners. They are provided for identification only; tvlogo.monster claims no ownership and is not affiliated with or endorsed by any broadcaster. The metadata (mappings, ids, tables) is CC BY 4.0; the logo images themselves are not licensed by tvlogo.monster. Legal notice: https://search.tvlogo.monster/legal
## Base URLs
| What | URL |
|---|---|
| REST API (JSON, read-only, CORS enabled) | `https://api.tvlogo.monster/v1` |
| Logo images, 512 px | `https://i.tvlogo.monster/{public_id}.png` |
| Logo images, other sizes | `https://i.tvlogo.monster/{public_id}-256.png`, `-128.png`, `-64.png` |
| OpenAPI document | `https://api.tvlogo.monster/openapi.json` |
| Human docs and playground | `https://search.tvlogo.monster/api`, `https://search.tvlogo.monster/` |
Always take image URLs from the API's `image_url` instead of building them yourself. Image URLs never change for the same version of a logo, so they are safe to store in playlists and to cache for a long time.
## Endpoints (generated from https://api.tvlogo.monster/openapi.json)
### GET /v1/affiliates
`https://api.tvlogo.monster/v1/affiliates` - Affiliate values for /v1/stations with station counts
| Parameter | In | Required | Type | Allowed values | Default | Description |
|---|---|---|---|---|---|---|
| `country` | query | no | string | | `""` | ISO country filter, e.g. CA |
| `has_logo` | query | no | string | `true`, `false`, `any` | `"true"` | count only stations with (true) / without (false) a logo, or any |
| `mode` | query | no | string | `local`, `national` | `"local"` | mode used for has_logo |
Responses: `200` Successful Response; `422` Validation Error
### GET /v1/classes
`https://api.tvlogo.monster/v1/classes` - Licence/service class values for /v1/stations with station counts
| Parameter | In | Required | Type | Allowed values | Default | Description |
|---|---|---|---|---|---|---|
| `country` | query | no | string | | `""` | ISO country filter, e.g. CA |
| `has_logo` | query | no | string | `true`, `false`, `any` | `"true"` | count only stations with (true) / without (false) a logo, or any |
| `mode` | query | no | string | `local`, `national` | `"local"` | mode used for has_logo |
Responses: `200` Successful Response; `422` Validation Error
### GET /v1/logo
`https://api.tvlogo.monster/v1/logo` - Resolve one key to one logo
| Parameter | In | Required | Type | Allowed values | Default | Description |
|---|---|---|---|---|---|---|
| `key` | query | yes | string (maxLength=200) | | | tvg-id (CTV.ca@CKYDT), epg.monster id (CTV.Winnipeg.ca), call sign (CKY-DT) or channel name (CTV Winnipeg) |
| `namespace` | query | no | string | `auto`, `iptv-org`, `epg.monster`, `call_sign`, `name` | `"auto"` | Which id system the key belongs to. auto tries tvg-id, epg.monster id, call sign, then channel name. |
| `mode` | query | no | string | `local`, `national` | `"local"` | local = the station's own logo, falling back to brand -> network -> channel. national = the network logo for every affiliate. |
| `theme` | query | no | string | `default`, `dark`, `light`, `mono` | `"default"` | Preferred variant. dark = for dark backgrounds, light = for light backgrounds, mono = monochrome. Falls back to default when the variant does not exist (theme_served tells you what you got). |
| `era` | query | no | string | `current`, `historic` | `"current"` | current = the logo in use now (new). historic = an older design (old) when we have one, else current. |
| `size` | query | no | string | `64`, `128`, `256`, `512` | `"512"` | Square PNG size in pixels (transparent canvas). All sizes are pre-rendered on i.tvlogo.monster. |
| `redirect` | query | no | boolean | | `false` | 302 straight to the PNG instead of JSON (handy as tvg-logo in M3U playlists) |
Responses: `200` Logo found (or key known but no logo: found=false, 404); `404` Key unknown or no logo; `410` Logo removed; `429` Rate limited; see Retry-After; `422` Validation Error
### GET /v1/params
`https://api.tvlogo.monster/v1/params` - Lookup parameters, values and defaults
Responses: `200` Successful Response
### GET /v1/search
`https://api.tvlogo.monster/v1/search` - Find channels by name, id or call sign (max 20)
| Parameter | In | Required | Type | Allowed values | Default | Description |
|---|---|---|---|---|---|---|
| `q` | query | yes | string (minLength=1, maxLength=100) | | | |
| `limit` | query | no | integer (minimum=1, maximum=20) | | `12` | |
| `mode` | query | no | string | `local`, `national` | `"local"` | |
| `theme` | query | no | string | `default`, `dark`, `light`, `mono` | `"default"` | |
| `size` | query | no | string | `64`, `128`, `256`, `512` | `"128"` | |
Responses: `200` Successful Response; `422` Validation Error
### GET /v1/stations
`https://api.tvlogo.monster/v1/stations` - Find stations by place, distance, province, city, type, class or affiliate
| Parameter | In | Required | Type | Allowed values | Default | Description |
|---|---|---|---|---|---|---|
| `near` | query | no | string | | `""` | Centre point as lat,lon in decimal degrees (WGS84), e.g. 49.8951,-97.1384 |
| `radius_km` | query | no | string | | `""` | Search radius around near, km (default 50, max 1000); distance is great-circle (haversine) |
| `country` | query | no | string | | `""` | ISO 3166-1 alpha-2 country; only CA has stations today |
| `province` | query | no | string | | `""` | 2-letter province/territory code, e.g. MB, QC |
| `city` | query | no | string | | `""` | Town or city of the transmitter (case-, accent- and punctuation-insensitive) |
| `q` | query | no | string (maxLength=100) | | `""` | Optional text matched inside call sign, name or town |
| `type` | query | no | string | `all`, `main`, `rebroadcaster` | `"all"` | main = originating transmitters, rebroadcaster = repeaters, all |
| `class` | query | no | string (maxLength=100) | | `""` | Licence/service class, one or more comma-separated: full_power, class_a, low_power (US LPTV; CA ISED class LP/VLP), translator (US translators, CA rebroadcasters). Canada has no class_a. Unknown values give 422. See /v1/classes for counts |
| `affiliate` | query | no | string | | `""` | Network or brand slug or name (ctv, global, citytv, cbc, ici-radio-canada, tva, noovo, independent...); see /v1/affiliates |
| `has_logo` | query | no | string | `true`, `false`, `any` | `"true"` | true (default, like the map) = only stations with a served logo for the selected mode; false = only those without; any |
| `sort` | query | no | string | `distance`, `name`, `call_sign` | `""` | distance (default with near), name (default otherwise) or call_sign |
| `limit` | query | no | string | | `"20"` | Rows per page, 1..100 |
| `cursor` | query | no | string (maxLength=200) | | `""` | next_cursor from the previous page |
| `mode` | query | no | string | `local`, `national` | `"local"` | local = the station's own logo, falling back to brand -> network -> channel. national = the network logo for every affiliate. |
| `theme` | query | no | string | `default`, `dark`, `light`, `mono` | `"default"` | Preferred variant. dark = for dark backgrounds, light = for light backgrounds, mono = monochrome. Falls back to default when the variant does not exist (theme_served tells you what you got). |
| `era` | query | no | string | `current`, `historic` | `"current"` | current = the logo in use now (new). historic = an older design (old) when we have one, else current. |
| `size` | query | no | string | `64`, `128`, `256`, `512` | `"128"` | Square PNG size in pixels (transparent canvas). All sizes are pre-rendered on i.tvlogo.monster. |
Responses: `200` Matching transmitters (paged; next_cursor is null on the last page); `422` Invalid filter (detail says which); `429` Rate limited; see Retry-After
### GET /v1/stats
`https://api.tvlogo.monster/v1/stats` - Catalogue counts and API usage (cached 60 s)
Responses: `200` Successful Response
### GET /v1/version
`https://api.tvlogo.monster/v1/version` - Deployed app version: package version, git commit, deploy time
Responses: `200` Successful Response
## Keys
One key in, one logo out. `namespace=auto` (the default) tries these in order:
| Key type | Example | namespace |
|---|---|---|
| iptv-org tvg-id, `Channel.cc` or `Channel.cc@Feed` (as in M3U `tvg-id`) | `CTV.ca@CKYDT` | `iptv-org` |
| epg.monster channel id | `CTV.Winnipeg.ca` | `epg.monster` |
| Canadian call sign | `CKY-DT` | `call_sign` |
| channel name (case and punctuation insensitive) | `CTV Winnipeg` | `name` |
Set `namespace` explicitly when you know the key type; it avoids a wrong guess. `/v1/search` finds candidates when a key does not resolve.
## Station search
`GET /v1/stations` lists broadcast stations (one row per transmitter) with their logo, filtered by any of: `near=lat,lon` + `radius_km` (great-circle distance, nearest first, `distance_km` on each row), `country`, `province`, `city` (case and accent insensitive), `q` (call sign, name or town), `type=main|rebroadcaster|all`, `class` (licence/service class, one or more comma-separated of `full_power`, `class_a`, `low_power`, `translator`; Canada: rebroadcasters are `translator`, ISED class LP/VLP main transmitters are `low_power`, the rest `full_power`, and there is no `class_a`; `GET /v1/classes` gives counts), `affiliate` (network or brand slug or name; `GET /v1/affiliates` lists them with counts) and `has_logo` (default `true`). `sort=distance|name|call_sign`; `limit` is at most 100 and `next_cursor` (null on the last page) is passed back as `cursor` with the same filters for the next page. Invalid filters answer `422` with `error` and `detail`. Each row has `call_sign`, `name`, `town`, `province`, `type`, `class`, `parent_call_sign`, `affiliate`, `network`, `owner`, `lat`, `lon`, `tvg_id`, `epg_id`, `image_url` (null when there is no logo) and `maps_url`.
## Options and fallback rules
- **mode** (default `local`): local = the station's own logo, falling back to brand -> network -> channel. national = the network logo for every affiliate.
- `local`: local station logo first, then brand -> network -> channel
- `national`: network logo for every affiliate (e.g. all CTV stations get the CTV logo)
- **theme** (default `default`): Preferred variant. dark = for dark backgrounds, light = for light backgrounds, mono = monochrome. Falls back to default when the variant does not exist (theme_served tells you what you got).
- `default`: the standard logo
- `dark`: variant for dark backgrounds, falls back to default
- `light`: variant for light backgrounds, falls back to default
- `mono`: monochrome variant, falls back to default
- **era** (default `current`): current = the logo in use now (new). historic = an older design (old) when we have one, else current.
- `current`: the logo in use today
- `historic`: an older design if known, else the current one
- **size** (default `512`): Square PNG size in pixels (transparent canvas). All sizes are pre-rendered on i.tvlogo.monster.
- `64`: 64x64 px PNG
- `128`: 128x128 px PNG
- `256`: 256x256 px PNG
- `512`: 512x512 px PNG
Fallback chain in `local` mode: station -> brand -> network -> channel; `resolved_level` tells you which level answered and `fallback` is true whenever you did not get exactly what you asked for (level, theme or era). Sizes are 64, 128, 256, 512 px; all are pre-rendered.
## Response
`GET /v1/logo?key=CTV.ca@CKYDT&size=128` returns (real example):
```json
{
"key": "CTV.ca@CKYDT",
"namespace": "iptv-org",
"matched_key": "ctv.ca@ckydt",
"name": "CTV CKY-DT",
"country": "CA",
"mode": "local",
"theme": "default",
"era": "current",
"size": 128,
"found": true,
"image_url": "https://i.tvlogo.monster/vlhdz-128.png",
"public_id": "vlhdz",
"resolved_level": "station",
"theme_served": "default",
"era_served": "current",
"fallback": false,
"source": "epg-monster",
"source_url": "http://dtil.tmsimg.com/assets/s72769_ll_h15_aa.png?w=360&h=270",
"licence": null,
"trademark_note": "Channel logos are trademarks and/or copyright of their respective owners. ...",
"data_licence": "CC-BY-4.0 (metadata and mappings only; logo images are not licensed by tvlogo.monster)"
}
```
| Field | Meaning |
|---|---|
| `key`, `namespace`, `matched_key` | what you sent, the id system that matched, and the normalised key that matched |
| `name`, `country` | channel or station name and ISO country code |
| `mode`, `theme`, `era`, `size` | the options that were applied (echo of the request) |
| `found` | true when image_url is set |
| `image_url` | the PNG to use (square, transparent canvas, the requested size); stable for this logo version |
| `public_id` | short logo id; image URLs are built from it |
| `resolved_level` | which level answered: station, brand, network or channel |
| `theme_served`, `era_served` | the variant you actually got (may differ from theme/era after a fallback) |
| `fallback` | true when a fallback was used (e.g. no dark variant, so default was served) |
| `source`, `source_url` | provenance of the original artwork; never hot-link source_url, use image_url |
| `licence` | licence of the artwork when one is known, usually null (logos are trademarks, not licensed) |
| `trademark_note`, `data_licence` | legal notes; show the trademark note wherever you display logos |
`redirect=true` answers `302` straight to the PNG instead of JSON. It works as a `tvg-logo` value, but then every player hits the API on every load, so tools should store the resolved `image_url` instead.
## Errors
| Status | Meaning | What to do |
|---|---|---|
| `404` | key unknown or no logo; JSON body with `found: false`, `error: "not_found"` | treat as "no logo", cache it (for a day), try the next key candidate |
| `410` | logo removed (takedown) | do not retry; remove that logo from your output |
| `422` | invalid parameter; JSON body with `error` and `detail` | fix the request, do not retry |
| `429` | rate limited | wait `Retry-After` seconds, then retry (see below) |
| `5xx` / network | temporary problem | retry with exponential backoff and jitter, give up after a few tries |
Example 404 body:
```json
{"key": "zzznonexist", "found": false, "image_url": null, "error": "not_found", "detail": "No channel, feed, station or name matches this key.", "trademark_note": "..."}
```
## Rate limits and etiquette
- Limits are per IP address (IPv6 per /64): lookups allow a burst of 60 requests and 2 per second sustained.
- Every response has `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` headers. Throttle yourself so you rarely see a 429.
- On `429`, wait the number of seconds in `Retry-After` before the next request (never retry immediately). Clients that keep ignoring it are blocked temporarily: 15 minutes, then 1 hour, 24 hours, 7 days.
- Run requests sequentially (or at most 2 in flight) against the API; image downloads from i.tvlogo.monster may use a few parallel connections.
## Caching
- Deduplicate keys before calling the API and cache every answer locally (key + options -> JSON), including 404s.
- Lookups are served with `Cache-Control: public, max-age=3600` and an `ETag`; revalidate with `If-None-Match` (a `304` costs almost nothing). Re-check cached 404s after about a day.
- Store `image_url` rather than re-resolving on every run; it only changes when the logo changes.
## Attribution expectations
- Credit "tvlogo.monster" with a link to https://search.tvlogo.monster in your README or about screen (the metadata is CC BY 4.0).
- Keep the trademark note visible where logos are shown, do not present logos as licensed, and do not imply endorsement by any broadcaster.
- Honour removals: when the API answers `410`, drop that logo.
## My current selection
These are the options I picked in the tvlogo.monster playground. Use them as the tool's defaults (configurable by flags or settings).
- example key: `CTV.ca@CKYDT`
- preferred language: any - choose what suits the task
- namespace: `auto` (default) - guess the key type (tvg-id, epg.monster id, call sign or name)
- mode: `local` (default) - local station logo first, then brand -> network -> channel
- theme: `default` (default) - the standard logo
- era: `current` (default) - the logo in use today
- size: `512` (default) - 512x512 px PNG
Equivalent request: `GET https://api.tvlogo.monster/v1/logo?key=CTV.ca%40CKYDT`
## Task: fix broken logos in my M3U playlist
Write a command-line tool that:
1. Reads an M3U/M3U8 playlist from a path or URL. Parses every `#EXTINF` line with its attributes `tvg-id`, `tvg-name`, `tvg-logo`, `group-title` (quoted values may contain commas and spaces) plus the display name after the last unquoted comma. Every other line, attribute and the order are kept exactly as they were.
2. Decides which entries need a logo: `tvg-logo` missing or empty, or broken (request fails, times out after about 10 s, is not 2xx, or is not an image content type). Each distinct logo URL is checked only once. Existing working logos are kept unless I pass `--replace-all`.
3. Resolves each of those channels with `GET /v1/logo`: `tvg-id` first (namespace `auto` understands `Channel.cc@Feed`), then `tvg-name`, then the display name. Uses the options from "My current selection" as defaults. The new `tvg-logo` is the returned `image_url` (an i.tvlogo.monster URL), never `source_url`.
4. Respects the rate limits: throttle to at most 2 requests per second, deduplicate keys, wait `Retry-After` on 429, exponential backoff with jitter on 5xx and network errors, and cache every answer (404s too) in a local JSON or SQLite file keyed by key + options so a re-run makes almost no requests.
5. Writes the fixed playlist to a new file (the input is only overwritten with `--in-place`), keeping encoding and line endings.
6. Writes a CSV report of changes and unmatched channels: line number, channel name, tvg-id, group-title, old tvg-logo, old status (ok / missing / broken), new tvg-logo, matched key, resolved_level, fallback, action (kept / replaced / unmatched). Prints a one-line summary.
7. Has `--dry-run`, `--mode`, `--theme`, `--size`, `--cache`, `--report` and a clear `--help`.
Also give me a short README and unit tests for the M3U parser (quoted attributes containing commas, missing attributes, the `#EXTM3U` header line, Windows line endings).
Also as plain Markdown: /ai-prompt.md (takes the same options as query parameters) · machine-readable spec and tasks: /ai-prompt.json · full API reference.

