diff --git a/app.py b/app.py index 240eaa5..753da6d 100644 --- a/app.py +++ b/app.py @@ -257,6 +257,7 @@ def group_channels(raw_entries: list, group_meta: dict) -> list: 'resolution': parsed['resolution'], 'acestream_hash': entry['acestream_hash'], 'status': 'unknown', + 'country_code': country_code_from_logo_url('', entry['tvg_logo']), } if key not in channel_map: @@ -331,6 +332,7 @@ def load_from_json_content(data: dict, source_file: str) -> None: m.pop('raw_name', None) m.pop('quality_marker', None) m.setdefault('status', 'unknown') + m.setdefault('country_code', '') group_meta = {g['name']: g.get('logo', '') for g in data.get('groups', [])} groups = sorted({ch['group'] for ch in channels}) state = { diff --git a/static/index.html b/static/index.html index 35b6992..7e13b8c 100644 --- a/static/index.html +++ b/static/index.html @@ -279,6 +279,18 @@ Exportar M3U +
+ @@ -441,11 +453,12 @@ if (this.activeGroup) ch = ch.filter(c => c.group === this.activeGroup); if (this.activeSubcategory) ch = ch.filter(c => c.subcategory === this.activeSubcategory); if (this.activeTags.size) ch = ch.filter(c => c.tags && c.tags.some(t => this.activeTags.has(t))); - if (this.activeCountries.size) ch = ch.filter(c => this.activeCountries.has(c.country_code)); + if (this.activeCountries.size) ch = ch.filter(c => c.mirrors.some(m => this.activeCountries.has(m.country_code))); const q = this.search.toLowerCase().trim(); if (q) ch = ch.filter(c => c.name.toLowerCase().includes(q) || - c.group.toLowerCase().includes(q) + c.group.toLowerCase().includes(q) || + c.mirrors.some(m => m.acestream_hash.includes(q)) ); return ch; } @@ -539,7 +552,7 @@ const CHEVRON_RIGHT = ``; function renderTopCountries() { - const codes = [...new Set(App.channels.map(c => c.country_code).filter(Boolean))].sort(); + const codes = [...new Set(App.channels.flatMap(c => c.mirrors.map(m => m.country_code)).filter(Boolean))].sort(); const wrap = $('countryWrap'); if (!wrap) return; if (codes.length === 0) { wrap.classList.add('hidden'); return; } @@ -767,7 +780,10 @@ // Flag in modal header (next to group badge) const flagEl = $('modalFlag'); - if (flagEl) flagEl.innerHTML = flagHTML(ch.country_code || ''); + if (flagEl) { + const mirrorCodes = [...new Set(ch.mirrors.map(m => m.country_code).filter(Boolean))].sort(); + flagEl.innerHTML = mirrorCodes.map(c => flagHTML(c)).join(''); + } // Sort mirrors best → worst quality const TIER_ORDER = ['diamond', 'gold', 'silver', 'bronze']; @@ -791,6 +807,9 @@
${i + 1} +
+ ${m.country_code ? flagHTML(m.country_code) : ''} +
${tierBadge(m.resolution) || ''}
@@ -1000,6 +1019,27 @@ .replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); } + const COUNTRY_FOLDER_MAP = { + 'albania':'al','argentina':'ar','australia':'au','austria':'at','azerbaijan':'az', + 'belgium':'be','brazil':'br','bulgaria':'bg','canada':'ca','chile':'cl', + 'costa-rica':'cr','croatia':'hr','czech-republic':'cz','france':'fr','germany':'de', + 'greece':'gr','hong-kong':'hk','hungary':'hu','india':'in','indonesia':'id', + 'ireland':'ie','israel':'il','italy':'it','lebanon':'lb','lithuania':'lt', + 'luxembourg':'lu','malaysia':'my','malta':'mt','mexico':'mx','netherlands':'nl', + 'new-zealand':'nz','philippines':'ph','poland':'pl','portugal':'pt','romania':'ro', + 'russia':'ru','serbia':'rs','singapore':'sg','slovakia':'sk','slovenia':'si', + 'spain':'es','switzerland':'ch','turkey':'tr','ukraine':'ua', + 'united-arab-emirates':'ae','united-kingdom':'gb','united-states':'us', + }; + + function countryFromUrl(url) { + if (!url) return ''; + const m1 = url.match(/\/countries\/([^/]+)\//); + if (m1) return COUNTRY_FOLDER_MAP[m1[1]] || ''; + const m2 = url.match(/-([a-z]{2})(?:-[a-z]+)?\.(?:png|svg|jpg)$/i); + return m2 ? m2[1].toLowerCase() : ''; + } + function parseImportM3U(text, filename) { const EXTINF_ATTR = /([\w-]+)="([^"]*)"/g; const ACESTREAM_RE = /^acestream:\/\/([a-f0-9]{40})/i; @@ -1068,7 +1108,10 @@ const { base_name, resolution } = parseChannelName(entry.raw_name); const group = entry.group_title; const key = `${base_name.toLowerCase()}\x00${group.toLowerCase()}`; - const mirror = { resolution, acestream_hash: entry.acestream_hash, status: 'unknown' }; + const mirror = { + resolution, acestream_hash: entry.acestream_hash, status: 'unknown', + country_code: countryFromUrl(entry.tvg_logo), + }; if (!channelMap.has(key)) { let baseId = slugify(base_name) || slugify(group); @@ -1077,7 +1120,7 @@ else { idSeen[baseId] = 0; channelId = baseId; } channelMap.set(key, { id: channelId, name: base_name, group, subcategory: '', - country_code: '', logo_url: entry.tvg_logo || '', tags: [], + country_code: countryFromUrl(entry.tvg_logo), logo_url: entry.tvg_logo || '', tags: [], _tvg_logo: entry.tvg_logo, mirrors: [], }); order.push(key); @@ -1089,6 +1132,10 @@ } const channels = order.map(k => { const ch = channelMap.get(k); delete ch._tvg_logo; return ch; }); + // Fallback: mirrors without country_code inherit the channel's country_code + channels.forEach(ch => + ch.mirrors.forEach(m => { if (!m.country_code) m.country_code = ch.country_code || ''; }) + ); const groups = [...new Set(channels.map(c => c.group))].sort(); return { channels, groups, groupMeta, source: filename }; } @@ -1101,7 +1148,8 @@ ch.country_code = ch.country_code || ''; (ch.mirrors || []).forEach(m => { delete m.acestream_url; delete m.raw_name; delete m.quality_marker; - m.status = m.status || 'unknown'; + m.status = m.status || 'unknown'; + m.country_code = m.country_code || ch.country_code || ''; }); return ch; }); @@ -1178,6 +1226,48 @@ else downloadBlob(buildExportM3U(), 'channels.m3u', 'text/plain'); } + function doExportTemplate() { + $('exportMenu').classList.add('hidden'); + const template = JSON.stringify({ + version: '1.0', + exported_at: new Date().toISOString(), + source: 'plantilla', + channels: [ + { + id: 'nombre-canal', + name: 'Nombre del Canal', + group: 'Nombre del Grupo', + subcategory: '', + country_code: 'es', + logo_url: 'https://ejemplo.com/logo.png', + tags: ['🔴', 'HD'], + mirrors: [ + { + resolution: '1080p', + acestream_hash: '0000000000000000000000000000000000000001', + status: 'unknown', + country_code: 'es', + }, + { + resolution: '720p', + acestream_hash: '0000000000000000000000000000000000000002', + status: 'ok', + country_code: 'gb', + }, + ], + }, + ], + groups: [ + { + name: 'Nombre del Grupo', + logo: 'https://ejemplo.com/group-logo.png', + subcategories: [], + }, + ], + }, null, 2); + downloadBlob(template, 'channels-plantilla.json', 'application/json'); + } + // ── Toast ──────────────────────────────────────────────────────────────────── let _toastTimer; function showToast(msg, isError = false) { @@ -1207,11 +1297,12 @@ (data.groups || []).filter(g => typeof g !== 'string').map(g => [g.name, g.logo || '']) ); $('sourceLabel').textContent = data.source || ''; - // Superponer estados guardados en localStorage + // Superponer estados y country_code guardados en localStorage / fallback App.channels.forEach(ch => ch.mirrors.forEach(m => { const saved = localStorage.getItem(`ms_${m.acestream_hash}`); if (saved) m.status = saved; + if (!m.country_code) m.country_code = ch.country_code || ''; }) ); renderAll();