diff --git a/.gitignore b/.gitignore index 36b13f1..32eea8e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +web/ + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/app.py b/app.py index d8b3bc6..cdd0c66 100644 --- a/app.py +++ b/app.py @@ -94,12 +94,12 @@ def resolve_logo(tvg_id: str, base_name: str, tvg_logo: str) -> tuple: """ def lookup(slug: str): if slug in logo_index: - return f'/logos/{logo_index[slug]}', 'local' + return f'logos/{logo_index[slug]}', 'local' # Prefix match: allows "dazn-laliga" → "dazn-laliga-es" when no-suffix # alias wasn't created (e.g. the suffix was longer than 3 chars) for key, path in logo_index.items(): if key.startswith(slug) and len(key) <= len(slug) + 4: - return f'/logos/{path}', 'local' + return f'logos/{path}', 'local' return None, None if tvg_id: @@ -256,6 +256,7 @@ def group_channels(raw_entries: list, group_meta: dict) -> list: mirror = { 'resolution': parsed['resolution'], 'acestream_hash': entry['acestream_hash'], + 'status': 'unknown', } if key not in channel_map: @@ -329,6 +330,7 @@ def load_from_json_content(data: dict, source_file: str) -> None: m.pop('acestream_url', None) m.pop('raw_name', None) m.pop('quality_marker', None) + m.setdefault('status', 'unknown') group_meta = {g['name']: g.get('logo', '') for g in data.get('groups', [])} groups = sorted({ch['group'] for ch in channels}) state = { @@ -463,6 +465,33 @@ def api_export_m3u(): ) +@app.route('/api/mirror/status', methods=['POST']) +def api_mirror_status(): + data = request.get_json() + channel_id = data.get('channel_id') + mirror_idx = data.get('mirror_idx') + status = data.get('status', 'unknown') + if status not in ('ok', 'issues', 'broken', 'unknown'): + return jsonify({'error': 'invalid status'}), 400 + ch = next((c for c in state['channels'] if c['id'] == channel_id), None) + if not ch or not isinstance(mirror_idx, int) or mirror_idx >= len(ch['mirrors']): + return jsonify({'error': 'not found'}), 404 + ch['mirrors'][mirror_idx]['status'] = status + export = { + 'version': '1.0', + 'exported_at': datetime.now(timezone.utc).isoformat(), + 'source': state['source_file'], + 'channels': state['channels'], + 'groups': [ + {'name': g, 'logo': state['group_meta'].get(g, ''), 'subcategories': []} + for g in state['groups'] + ], + } + with open(DEFAULT_JSON, 'w', encoding='utf-8') as f: + json.dump(export, f, ensure_ascii=False, indent=2) + return jsonify({'ok': True}) + + # ── Startup ─────────────────────────────────────────────────────────────────── def startup() -> None: diff --git a/channels.json b/channels.json index 98ecc81..0764fc4 100644 --- a/channels.json +++ b/channels.json @@ -9,7 +9,7 @@ "group": "DAZN", "subcategory": "", "country_code": "es", - "logo_url": "/logos/countries/spain/dazn-1-es.png", + "logo_url": "logos/countries/spain/dazn-1-es.png", "tags": [], "mirrors": [ { @@ -32,7 +32,7 @@ "group": "DAZN", "subcategory": "", "country_code": "es", - "logo_url": "/logos/countries/spain/dazn-2-es.png", + "logo_url": "logos/countries/spain/dazn-2-es.png", "tags": [], "mirrors": [ { @@ -47,7 +47,7 @@ "group": "DAZN", "subcategory": "LaLiga", "country_code": "es", - "logo_url": "/logos/countries/spain/dazn-laliga-es.png", + "logo_url": "logos/countries/spain/dazn-laliga-es.png", "tags": [], "mirrors": [ { @@ -70,7 +70,7 @@ "group": "DAZN", "subcategory": "LaLiga", "country_code": "es", - "logo_url": "/logos/countries/spain/dazn-laliga-2-es.png", + "logo_url": "logos/countries/spain/dazn-laliga-2-es.png", "tags": [], "mirrors": [ { @@ -89,7 +89,7 @@ "group": "DAZN", "subcategory": "F1", "country_code": "es", - "logo_url": "/logos/countries/spain/dazn-f1-es.png", + "logo_url": "logos/countries/spain/dazn-f1-es.png", "tags": [], "mirrors": [ { @@ -116,7 +116,7 @@ "group": "LaLiga", "subcategory": "Hypermotion", "country_code": "es", - "logo_url": "/logos/countries/spain/laliga-tv-hypermotion-es.png", + "logo_url": "logos/countries/spain/laliga-tv-hypermotion-es.png", "tags": [], "mirrors": [ { @@ -162,7 +162,7 @@ "group": "Movistar+", "subcategory": "", "country_code": "es", - "logo_url": "/logos/countries/spain/movistar-plus-es.png", + "logo_url": "logos/countries/spain/movistar-plus-es.png", "tags": [], "mirrors": [ { @@ -186,7 +186,9 @@ "subcategory": "Deportes", "country_code": "es", "logo_url": "logos/countries/spain/deportes-por-movistar-plus-es.png", - "tags": ["\u26BD"], + "tags": [ + "⚽" + ], "mirrors": [ { "resolution": "720p", @@ -205,7 +207,9 @@ "subcategory": "Deportes", "country_code": "es", "logo_url": "logos/countries/spain/deportes-2-por-movistar-plus-es.png", - "tags": ["\u26BD"], + "tags": [ + "⚽" + ], "mirrors": [ { "resolution": "720p", @@ -232,7 +236,9 @@ "subcategory": "Deportes", "country_code": "es", "logo_url": "logos/countries/spain/deportes-3-por-movistar-plus-es.png", - "tags": ["\u26BD"], + "tags": [ + "⚽" + ], "mirrors": [ { "resolution": "1080p", @@ -250,16 +256,30 @@ "group": "Movistar+", "subcategory": "Golf", "country_code": "es", - "logo_url": "/logos/countries/spain/golf-por-movistar-plus-es.png", - "tags": ["\u26F3"], + "logo_url": "logos/countries/spain/golf-por-movistar-plus-es.png", + "tags": [ + "⛳" + ], "mirrors": [ + { + "resolution": "1080p", + "acestream_hash": "9bb2a7dc6c6a296609df8cc50e742a78194f7530" + }, + { + "resolution": "1080p", + "acestream_hash": "97df5b7824948972d041d8ca2a4d29c90b641bc9" + }, + { + "resolution": "1080p", + "acestream_hash": "9db029dff6a9c637d1f670e78dbc1a479b9b406e" + }, { "resolution": "720p", "acestream_hash": "f60535a458b3c114283e2f433379144345beddd6" }, { - "resolution": "1080p", - "acestream_hash": "9bb2a7dc6c6a296609df8cc50e742a78194f7530" + "resolution": "720p", + "acestream_hash": "63e4713e8379f2ebf5e6ed9aa00fbf78ebae7439" } ] }, @@ -269,11 +289,13 @@ "group": "Movistar+", "subcategory": "Golf", "country_code": "es", - "logo_url": "/logos/countries/spain/golf-2-por-movistar-plus-es.png", - "tags": ["\u26F3"], + "logo_url": "logos/countries/spain/golf-2-por-movistar-plus-es.png", + "tags": [ + "⛳" + ], "mirrors": [ { - "resolution": "4K", + "resolution": "1080p", "acestream_hash": "ac321484243571f59e077f2a2521e648dc0c7b03" } ] @@ -285,16 +307,10 @@ "subcategory": "Vamos", "country_code": "es", "logo_url": "logos/countries/spain/vamos-por-movistar-plus-es.png", - "tags": ["\u26BD"], + "tags": [ + "⚽" + ], "mirrors": [ - { - "resolution": "720p", - "acestream_hash": "866a8af8faacf8fc9eb997ab5a68b4dfee4edc77" - }, - { - "resolution": "720p", - "acestream_hash": "0e5d8c9724fa9163f49096b70484e315251eb785" - }, { "resolution": "1080p", "acestream_hash": "ad5ca6e24f87f47a9c3c5bc10c7e8e5af46767fc" @@ -302,6 +318,18 @@ { "resolution": "1080p", "acestream_hash": "476b6f6583517bd75c15c4663bf45fab7c8da9cf" + }, + { + "resolution": "1080p", + "acestream_hash": "c7c81acdd1a03ecc418c94c2f28e2adb0556c40b" + }, + { + "resolution": "720p", + "acestream_hash": "866a8af8faacf8fc9eb997ab5a68b4dfee4edc77" + }, + { + "resolution": "720p", + "acestream_hash": "0e5d8c9724fa9163f49096b70484e315251eb785" } ] }, @@ -312,7 +340,9 @@ "subcategory": "Vamos", "country_code": "es", "logo_url": "logos/countries/spain/vamos-2-por-movistar-plus-es.png", - "tags": ["\u26BD"], + "tags": [ + "⚽" + ], "mirrors": [ { "resolution": "720p", @@ -338,8 +368,10 @@ "group": "RFEF", "subcategory": "", "country_code": "es", - "logo_url": "/logos/countries/spain/rfef-tv.png", - "tags": ["\u26BD"], + "logo_url": "logos/countries/spain/rfef-tv.png", + "tags": [ + "⚽" + ], "mirrors": [ { "resolution": "1080p", @@ -361,8 +393,11 @@ "group": "3Cat", "subcategory": "", "country_code": "cat", - "logo_url": "/logos/countries/spain/esport-3-es.png", - "tags": ["\u26BD", "\uD83C\uDFC0"], + "logo_url": "logos/countries/spain/esport-3-es.png", + "tags": [ + "⚽", + "🏀" + ], "mirrors": [ { "resolution": "720p", @@ -384,8 +419,12 @@ "group": "RTVE", "subcategory": "", "country_code": "es", - "logo_url": "/logos/countries/spain/tdp-es.png", - "tags": ["\u26BD","\uD83C\uDFC0", "\uD83E\uDD47"], + "logo_url": "logos/countries/spain/tdp-es.png", + "tags": [ + "⚽", + "🏀", + "🥇" + ], "mirrors": [ { "resolution": "720p", @@ -412,7 +451,9 @@ "subcategory": "MotoGP", "country_code": "es", "logo_url": "https://hips.hearstapps.com/vader-prod.s3.amazonaws.com/1772006365-dazn-motogp-699eaba85423e.jpg", - "tags": ["\uD83C\uDFCD\uFE0F"], + "tags": [ + "🏍️" + ], "mirrors": [ { "resolution": "1080p", @@ -432,42 +473,32 @@ "subcategories": [] }, { - "name": "DEPORTES", - "logo": "", - "subcategories": [] - }, - { - "name": "HYPERMOTION", - "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/LaLiga_Hypermotion_2023_Vertical_Logo.svg/2335px-LaLiga_Hypermotion_2023_Vertical_Logo.svg.png", - "subcategories": [] - }, - { - "name": "LA LIGA", - "logo": "https://i.ibb.co/sp6dD8h2/laliga-logo.png", - "subcategories": [] - }, - { - "name": "LIGA DE CAMPEONES", - "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Liga_de_Campeones_por_Movistar_Plus%2B_2023_Logo.svg/2560px-Liga_de_Campeones_por_Movistar_Plus%2B_2023_Logo.svg.png", - "subcategories": [] - }, - { - "name": "MOTOGP", - "logo": "", - "subcategories": [] - }, - { - "name": "MOVISTAR", + "name": "MOVISTAR+", "logo": "https://upload.wikimedia.org/wikipedia/commons/d/d9/Movistar%2B_Logo.png", "subcategories": [] }, { - "name": "MOVISTAR DEPORTES", - "logo": "https://upload.wikimedia.org/wikipedia/commons/thumb/8/83/Deportes_por_Movistar_Plus%2B_2022_logo.svg/2560px-Deportes_por_Movistar_Plus%2B_2022_logo.svg.png", + "name": "RTVE", + "logo": "", "subcategories": [] }, { - "name": "RFEF TV", + "name": "ATRESMEDIA", + "logo": "", + "subcategories": [] + }, + { + "name": "MEDIASET", + "logo": "", + "subcategories": [] + }, + { + "name": "LALIGA", + "logo": "https://i.ibb.co/sp6dD8h2/laliga-logo.png", + "subcategories": [] + }, + { + "name": "RFEF", "logo": "https://rfef.es/themes/custom/rfef/img/rfef-share.png", "subcategories": [] } diff --git a/static/index.html b/static/index.html index 27f6cf7..7f0d1ed 100644 --- a/static/index.html +++ b/static/index.html @@ -452,6 +452,22 @@ return null; } + // ── Mirror status ──────────────────────────────────────────────────────────── + const STATUS_CONFIG = { + unknown: { emoji: '❔', label: 'Por comprobar', bg: 'transparent' }, + ok: { emoji: '✅', label: 'Funciona', bg: 'rgba(34,197,94,0.08)' }, + issues: { emoji: '⚠️', label: 'Problemas', bg: 'rgba(245,158,11,0.10)' }, + broken: { emoji: '⛔️', label: 'No funciona', bg: 'rgba(239,68,68,0.10)' }, + }; + const STATUS_CYCLE = ['unknown', 'ok', 'issues', 'broken']; + + function getMirrorStatus(hash) { + return localStorage.getItem(`ms_${hash}`) || 'unknown'; + } + function setMirrorStatus(hash, status) { + localStorage.setItem(`ms_${hash}`, status); + } + // ── Flag helper ────────────────────────────────────────────────────────────── const TV_ICON = ` `; } @@ -685,18 +701,30 @@ // Mirrors $('modalMirrors').innerHTML = sorted.map((m, i) => { - const safeUrl = escAttr(`acestream://${m.acestream_hash}`); - const hashFull = escHTML(m.acestream_hash); - const hashShort = m.acestream_hash.slice(0, 16) + '…'; + const safeUrl = escAttr(`acestream://${m.acestream_hash}`); + const hashFull = escHTML(m.acestream_hash); + const hashShort = m.acestream_hash.slice(0, 16) + '…'; + const status = getMirrorStatus(m.acestream_hash); + const { emoji, label, bg } = STATUS_CONFIG[status] || STATUS_CONFIG.unknown; + const safeHash = escAttr(m.acestream_hash); + const safeChId = escAttr(ch.id); return ` -
+
${i + 1}
${tierBadge(m.resolution) || ''}
+