valoracion-mirrors
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,3 +1,5 @@
|
||||
web/
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
33
app.py
33
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:
|
||||
|
||||
157
channels.json
157
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": []
|
||||
}
|
||||
|
||||
@@ -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 = `<svg class="w-7 h-7 opacity-30" fill="none" viewBox="0 0 24 24" stroke="white" stroke-width="1.2">
|
||||
<path stroke-linecap="round" stroke-linejoin="round"
|
||||
@@ -461,7 +477,7 @@
|
||||
|
||||
function flagHTML(code) {
|
||||
if (!code) return '';
|
||||
return `<img src="/flags/${escAttr(code)}.svg" alt="${escAttr(code)}"
|
||||
return `<img src="flags/${escAttr(code)}.svg" alt="${escAttr(code)}"
|
||||
class="h-3.5 w-auto rounded-sm opacity-90"
|
||||
onerror="this.style.display='none'" loading="lazy">`;
|
||||
}
|
||||
@@ -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 `
|
||||
<div class="mirror-row flex items-center gap-3 px-4 py-2.5 rounded-lg mx-2 mb-0.5">
|
||||
<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-20">
|
||||
${tierBadge(m.resolution) || '<span class="text-xs text-gray-700">—</span>'}
|
||||
</div>
|
||||
<code class="flex-1 text-xs font-mono text-gray-600 truncate hidden sm:block"
|
||||
title="${hashFull}">${hashShort}</code>
|
||||
<button class="flex-shrink-0 w-8 h-8 flex items-center justify-center rounded-lg
|
||||
text-base transition-all cursor-pointer"
|
||||
title="${escAttr(label)}"
|
||||
style="background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.08)"
|
||||
data-status-hash="${safeHash}"
|
||||
data-channel-id="${safeChId}"
|
||||
data-mirror-idx="${i}">${emoji}</button>
|
||||
<button class="flex-shrink-0 flex items-center gap-1.5 px-3 py-1.5 rounded-lg
|
||||
text-xs font-semibold transition-all w-20 justify-center"
|
||||
style="background:rgba(124,58,237,0.2);color:var(--accent-light);
|
||||
@@ -768,8 +796,27 @@
|
||||
renderAll();
|
||||
});
|
||||
|
||||
// Mirror open button
|
||||
// Mirror open button + status selector
|
||||
$('modalMirrors').addEventListener('click', e => {
|
||||
const statusBtn = e.target.closest('[data-status-hash]');
|
||||
if (statusBtn) {
|
||||
const hash = statusBtn.dataset.statusHash;
|
||||
const cur = getMirrorStatus(hash);
|
||||
const next = STATUS_CYCLE[(STATUS_CYCLE.indexOf(cur) + 1) % STATUS_CYCLE.length];
|
||||
setMirrorStatus(hash, next);
|
||||
statusBtn.textContent = STATUS_CONFIG[next].emoji;
|
||||
statusBtn.title = STATUS_CONFIG[next].label;
|
||||
fetch('/api/mirror/status', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
channel_id: statusBtn.dataset.channelId,
|
||||
mirror_idx: parseInt(statusBtn.dataset.mirrorIdx),
|
||||
status: next,
|
||||
}),
|
||||
}).catch(() => {});
|
||||
return;
|
||||
}
|
||||
const btn = e.target.closest('[data-url]');
|
||||
if (btn) window.location.href = btn.dataset.url;
|
||||
});
|
||||
@@ -882,6 +929,13 @@
|
||||
(data.groups || []).filter(g => typeof g !== 'string').map(g => [g.name, g.logo || ''])
|
||||
);
|
||||
$('sourceLabel').textContent = data.source || '';
|
||||
// Superponer estados guardados en localStorage
|
||||
App.channels.forEach(ch =>
|
||||
ch.mirrors.forEach(m => {
|
||||
const saved = localStorage.getItem(`ms_${m.acestream_hash}`);
|
||||
if (saved) m.status = saved;
|
||||
})
|
||||
);
|
||||
renderAll();
|
||||
} catch (err) {
|
||||
$('stateLoading').classList.add('hidden');
|
||||
|
||||
Reference in New Issue
Block a user