diff --git a/arte_api.py b/arte_api.py index 1e0c463..acea66f 100644 --- a/arte_api.py +++ b/arte_api.py @@ -16,21 +16,25 @@ PLAYER_API = "https://api.arte.tv/api/player/v2/config/fr/{pid}" SEARCH_URL = "https://www.arte.tv/fr/search/?q={q}" GENRE_PAGES = [ - "https://www.arte.tv/fr/p/pop-rock/", - "https://www.arte.tv/fr/p/classique/", - "https://www.arte.tv/fr/p/musiques-electroniques/", - "https://www.arte.tv/fr/p/jazz", - "https://www.arte.tv/fr/p/arts-de-la-scene", - "https://www.arte.tv/fr/p/hip-hop", - "https://www.arte.tv/fr/p/metal", - "https://www.arte.tv/fr/p/opera", - "https://www.arte.tv/fr/p/world", - "https://www.arte.tv/fr/p/musique-baroque/", - # arte-concert pages pour l'agenda et contenu exclusif + ("Pop & Rock", "https://www.arte.tv/fr/p/pop-rock/"), + ("Classique", "https://www.arte.tv/fr/p/classique/"), + ("Electro", "https://www.arte.tv/fr/p/musiques-electroniques/"), + ("Jazz", "https://www.arte.tv/fr/p/jazz"), + ("Arts de la scène", "https://www.arte.tv/fr/p/arts-de-la-scene"), + ("Hip-hop", "https://www.arte.tv/fr/p/hip-hop"), + ("Metal", "https://www.arte.tv/fr/p/metal"), + ("Opéra", "https://www.arte.tv/fr/p/opera"), + ("World", "https://www.arte.tv/fr/p/world"), + ("Baroque", "https://www.arte.tv/fr/p/musique-baroque/"), +] + +EXTRA_PAGES = [ "https://www.arte.tv/fr/arte-concert/agenda/", "https://www.arte.tv/fr/arte-concert/", ] +CATEGORIES = [name for name, _ in GENRE_PAGES] + _HEADERS = { "User-Agent": ( "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 " @@ -88,14 +92,23 @@ def _metadata_for_pid(pid: str) -> dict | None: def _fetch_all_sync() -> list[dict]: - all_ids: set[str] = set() - for url in GENRE_PAGES: + id_cats: dict[str, list[str]] = {} + for name, url in GENRE_PAGES: + ids = _prog_ids_from_page(url) + logger.info(" %s → %d IDs", name, len(ids)) + for pid in ids: + id_cats.setdefault(pid, []).append(name) + + all_ids: set[str] = set(id_cats) + for url in EXTRA_PAGES: ids = _prog_ids_from_page(url) logger.info(" %s → %d IDs", url.split("/fr/")[1], len(ids)) all_ids |= ids logger.info("Total unique programme IDs: %d", len(all_ids)) concerts = _resolve_ids(all_ids) + for c in concerts: + c["categories"] = id_cats.get(c["id"], []) concerts.sort(key=lambda c: c.get("expiry") or "", reverse=True) return concerts @@ -131,30 +144,33 @@ async def get_all_concerts() -> list[dict]: return _cache["data"] -async def fetch_concerts(page: int = 1, search: str = "", page_size: int = 24) -> dict: +async def fetch_concerts(page: int = 1, search: str = "", page_size: int = 24, category: str = "") -> dict: all_c = await get_all_concerts() + + if category: + all_c = [c for c in all_c if category in (c.get("categories") or [])] + cached_ids = {c["id"] for c in all_c} if search: q = search.lower() - # local filter local = [ c for c in all_c if q in (c.get("title") or "").lower() or q in (c.get("subtitle") or "").lower() or q in (c.get("description") or "").lower() ] - # Arte search for IDs not in cache - loop = asyncio.get_event_loop() - remote_ids = await loop.run_in_executor(None, _search_sync, search) - new_ids = remote_ids - cached_ids - if new_ids: - extra = await loop.run_in_executor(None, _resolve_ids, new_ids, None) - # merge: local results first, then extras not already present - local_ids = {c["id"] for c in local} - for c in extra: - if c["id"] not in local_ids: - local.append(c) + # Remote search only when no category filter (results have no category info) + if not category: + loop = asyncio.get_event_loop() + remote_ids = await loop.run_in_executor(None, _search_sync, search) + new_ids = remote_ids - cached_ids + if new_ids: + extra = await loop.run_in_executor(None, _resolve_ids, new_ids, None) + local_ids = {c["id"] for c in local} + for c in extra: + if c["id"] not in local_ids: + local.append(c) filtered = local else: filtered = all_c diff --git a/main.py b/main.py index 9070b43..aa5a253 100644 --- a/main.py +++ b/main.py @@ -8,7 +8,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates from pydantic import BaseModel -from arte_api import fetch_concerts, invalidate_cache +from arte_api import fetch_concerts, invalidate_cache, CATEGORIES from downloader import DownloadManager logging.basicConfig(level=logging.INFO) @@ -31,9 +31,14 @@ async def index(request: Request): # ------------------------------------------------------------------ API: concerts +@app.get("/api/categories") +async def api_categories(): + return CATEGORIES + + @app.get("/api/concerts") -async def api_concerts(page: int = 1, search: str = "", page_size: int = 24): - return await fetch_concerts(page=page, search=search, page_size=page_size) +async def api_concerts(page: int = 1, search: str = "", page_size: int = 24, category: str = ""): + return await fetch_concerts(page=page, search=search, page_size=page_size, category=category) @app.post("/api/refresh") diff --git a/static/app.js b/static/app.js index 99571e4..f57ec41 100644 --- a/static/app.js +++ b/static/app.js @@ -4,6 +4,7 @@ const state = { page: 1, search: '', + category: '', pageSize: 24, totalPages: 1, current: null, // concert object shown in modal @@ -21,6 +22,7 @@ const modalOverlay = $('modal-overlay'); const dlPanel = $('dl-panel'); const dlPanelBody = $('dl-panel-body'); const dlBadge = $('dl-badge'); +const catBar = $('cat-bar'); // ── Helpers ────────────────────────────────────────────────────────────────── function fmtDuration(secs) { @@ -45,12 +47,37 @@ function debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; } +// ── Categories ─────────────────────────────────────────────────────────────── +async function loadCategories() { + try { + const cats = await fetch('/api/categories').then(r => r.json()); + cats.forEach(cat => { + const btn = document.createElement('button'); + btn.className = 'cat-pill'; + btn.dataset.cat = cat; + btn.textContent = cat; + catBar.appendChild(btn); + }); + } catch {} +} + +catBar.addEventListener('click', e => { + const pill = e.target.closest('.cat-pill'); + if (!pill) return; + catBar.querySelectorAll('.cat-pill').forEach(p => p.classList.remove('active')); + pill.classList.add('active'); + state.category = pill.dataset.cat; + state.page = 1; + refresh(); +}); + // ── Concerts ───────────────────────────────────────────────────────────────── async function loadConcerts() { const params = new URLSearchParams({ page: state.page, search: state.search, page_size: state.pageSize, + category: state.category, }); const res = await fetch(`/api/concerts?${params}`); if (!res.ok) throw new Error(res.statusText); @@ -359,6 +386,6 @@ document.addEventListener('keydown', e => { // ── Init ───────────────────────────────────────────────────────────────────── (async () => { - await refreshDlHistory(); + await Promise.all([loadCategories(), refreshDlHistory()]); await refresh(); })(); diff --git a/static/style.css b/static/style.css index 36a3f99..9742b07 100644 --- a/static/style.css +++ b/static/style.css @@ -195,6 +195,45 @@ body { letter-spacing: 0.02em; } +/* ══ CATEGORY BAR ══════════════════════════════════════════════════════════ */ +.cat-bar { + display: flex; + gap: 8px; + margin-bottom: 20px; + overflow-x: auto; + scrollbar-width: none; + -ms-overflow-style: none; + padding-bottom: 2px; +} + +.cat-bar::-webkit-scrollbar { display: none; } + +.cat-pill { + background: var(--surface); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-dim); + cursor: pointer; + font-family: 'Inter', sans-serif; + font-size: 13px; + padding: 6px 16px; + white-space: nowrap; + transition: color var(--transition), border-color var(--transition), background var(--transition); + flex-shrink: 0; +} + +.cat-pill:hover { + color: var(--text); + border-color: rgba(255,255,255,0.15); +} + +.cat-pill.active { + background: var(--gold); + border-color: var(--gold); + color: #000; + font-weight: 600; +} + /* ══ GRID ══════════════════════════════════════════════════════════════════ */ .grid { display: grid; diff --git a/templates/index.html b/templates/index.html index f64132f..bbb2a1b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -52,6 +52,11 @@
+ +
+ +
+
Chargement du catalogue…