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
+
${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();