mirrors-multidioma
This commit is contained in:
2
app.py
2
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 = {
|
||||
|
||||
@@ -279,6 +279,18 @@
|
||||
</svg>
|
||||
Exportar M3U
|
||||
</button>
|
||||
<div style="height:1px;background:rgba(255,255,255,0.06);margin:0 12px"></div>
|
||||
<button class="flex items-center gap-2.5 w-full px-4 py-3 text-sm text-gray-300
|
||||
hover:text-white hover:bg-white/5 text-left transition-colors"
|
||||
onclick="doExportTemplate()">
|
||||
<svg class="w-4 h-4 flex-shrink-0 text-gray-500"
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586
|
||||
a1 1 0 01.707.293l5.414 5.414A1 1 0 0121 8.414V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
Exportar plantilla JSON
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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 = `<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M9 5l7 7-7 7"/></svg>`;
|
||||
|
||||
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 @@
|
||||
<div class="mirror-row flex items-center gap-3 px-4 py-2.5 rounded-lg mx-2 mb-0.5"
|
||||
style="background:${bg}">
|
||||
<span class="text-xs text-gray-700 font-mono w-5 flex-shrink-0 text-right">${i + 1}</span>
|
||||
<div class="flex-shrink-0 w-5 flex items-center justify-center">
|
||||
${m.country_code ? flagHTML(m.country_code) : ''}
|
||||
</div>
|
||||
<div class="flex-shrink-0 w-20">
|
||||
${tierBadge(m.resolution) || '<span class="text-xs text-gray-700">—</span>'}
|
||||
</div>
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user