# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Commands

**Setup:**
```bash
python -m venv venv
venv/bin/pip install -r requirements.txt
```

**Run:**
```bash
bash run.sh          # kills anything on port 5000, then starts the app
# or directly:
venv/bin/python app.py
```

The app runs on `http://localhost:5000`. There are no tests.

## Architecture

This is a single-page IPTV channel manager for Acestream links. The entire backend is `app.py` (Flask) and the entire frontend is `static/index.html` (vanilla JS + bundled Tailwind).

### Data flow

1. **Startup**: `app.py` builds a logo index by walking `tv-logos-main/` (PNG files), then loads `channels.json` if it exists, otherwise falls back to `example.m3u`.
2. **M3U parsing** (`parse_m3u`): Reads `#EXTINF` lines and `acestream://` URLs only — non-Acestream URL lines are silently discarded. `#EXTGRP` lines provide group logos.
3. **Channel grouping** (`group_channels`): Channels sharing the same `base_name` + `group` (case-insensitive) are merged into a single channel with multiple `mirrors`. Each mirror stores `resolution` and `acestream_hash`.
4. **Logo resolution** (`resolve_logo`): Priority is local logo by tvg-id slug → local logo by channel name slug → external `tvg-logo` URL → empty. The logo index strips trailing 2-3 letter country suffixes for fuzzy matching.
5. **Global state**: The `state` dict holds `channels`, `groups`, `group_meta`, and `source_file`. `channels.json` is hot-reloaded on `/api/channels` requests if its mtime changed.

### Channel object schema

```json
{
  "id": "dazn-laliga",           // URL-safe slug, deduplicated with -N suffix
  "name": "DAZN LaLiga",         // base name without resolution/quality markers
  "group": "Sports",
  "subcategory": "",             // set manually via JSON import
  "country_code": "es",          // 2-letter ISO, derived from logo path or tvg-logo URL
  "logo_url": "/logos/...",      // local or external URL
  "tags": [],                    // emoji/string tags set manually
  "mirrors": [
    { "resolution": "1080p", "acestream_hash": "abc123..." }
  ]
}
```

### API endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/channels` | Returns current channel list (hot-reloads JSON if changed) |
| POST | `/api/import/m3u` | Upload M3U file, replaces state |
| POST | `/api/import/json` | Upload JSON export, replaces state |
| GET | `/api/export/json` | Download channels as JSON |
| GET | `/api/export/m3u` | Download channels as M3U (Acestream) |
| GET | `/logos/<path>` | Serve files from `tv-logos-main/` |
| GET | `/flags/<code>.svg` | Serve country flag SVGs from `svg/` |

### Key files

- `app.py` — entire backend; no separate modules
- `static/index.html` — entire frontend; all JS inline, Tailwind via `static/tailwind.js`
- `channels.json` — persisted channel data; auto-loaded at startup; hot-reloaded on change
- `example.m3u` — fallback playlist used if `channels.json` is absent
- `tv-logos-main/` — local PNG logo library (indexed at startup, not committed to git)
- `svg/` — country flag SVGs named by ISO 3166-1 alpha-2 code (e.g. `es.svg`)

### Frontend state

The JS `App` object in `index.html` holds all UI state: `channels`, `groups`, `groupMeta`, `activeGroup`, `activeSubcategory`, `activeTags`, `search`. All rendering is done by `renderAll()` → `renderSidebar()` + `renderGrid()`. Event delegation is used throughout (no per-card listeners). Clicking a channel card opens a modal with sorted mirrors (diamond > gold > silver > bronze quality tiers).
