diff --git a/app.py b/app.py
index cdd0c66..240eaa5 100644
--- a/app.py
+++ b/app.py
@@ -469,14 +469,17 @@ def api_export_m3u():
def api_mirror_status():
data = request.get_json()
channel_id = data.get('channel_id')
- mirror_idx = data.get('mirror_idx')
+ acestream_hash = data.get('acestream_hash')
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']):
+ if not ch:
return jsonify({'error': 'not found'}), 404
- ch['mirrors'][mirror_idx]['status'] = status
+ m = next((m for m in ch['mirrors'] if m['acestream_hash'] == acestream_hash), None)
+ if not m:
+ return jsonify({'error': 'not found'}), 404
+ m['status'] = status
export = {
'version': '1.0',
'exported_at': datetime.now(timezone.utc).isoformat(),
diff --git a/static/index.html b/static/index.html
index 7f0d1ed..a244d70 100644
--- a/static/index.html
+++ b/static/index.html
@@ -172,6 +172,28 @@
+
+
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.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));
const q = this.search.toLowerCase().trim();
if (q) ch = ch.filter(c =>
c.name.toLowerCase().includes(q) ||
@@ -501,11 +532,58 @@
renderSidebar();
renderGrid();
renderTopTags();
+ renderTopCountries();
}
const CHEVRON_DOWN = `
`;
const CHEVRON_RIGHT = `
`;
+ function renderTopCountries() {
+ const codes = [...new Set(App.channels.map(c => c.country_code).filter(Boolean))].sort();
+ const wrap = $('countryWrap');
+ if (!wrap) return;
+ if (codes.length === 0) { wrap.classList.add('hidden'); return; }
+ wrap.classList.remove('hidden');
+
+ const count = App.activeCountries.size;
+ const badge = $('countryBtnCount');
+ if (count > 0) { badge.textContent = count; badge.classList.remove('hidden'); }
+ else { badge.classList.add('hidden'); }
+
+ const allActive = count === 0;
+ $('countryMenu').innerHTML =
+ `
+ π
+ Todos
+ ${allActive ? 'β ' : ''}
+
+
`
+ + codes.map(code => {
+ const isActive = App.activeCountries.has(code);
+ return `
+
+ ${escHTML(code)}
+ ${isActive ? 'β ' : ''}
+ `;
+ }).join('')
+ + (count > 0
+ ? `
+
+ β Borrar selecciΓ³n
+ `
+ : '');
+ }
+
function renderTopTags() {
const bar = $('topTagsBar');
const allTags = [...new Set(App.channels.flatMap(c => c.tags || []))].sort();
@@ -704,7 +782,7 @@
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 status = m.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);
@@ -724,7 +802,7 @@
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}
+ data-current-status="${escAttr(status)}">${emoji}
+ ${emoji}
+ ${escHTML(label)}
+ ${isActive ? 'β ' : ''}
+ `;
+ }).join('');
+ const pop = $('statusPopover');
+ pop.classList.remove('hidden');
+ const rect = btn.getBoundingClientRect();
+ const popH = pop.offsetHeight || 160;
+ const top = (rect.bottom + 4 + popH > window.innerHeight)
+ ? rect.top - popH - 4
+ : rect.bottom + 4;
+ pop.style.top = `${top}px`;
+ pop.style.left = `${rect.left}px`;
+ }
+
+ $('statusPopover').addEventListener('click', e => {
+ const opt = e.target.closest('[data-set-status]');
+ if (!opt || !_activeStatusBtn) return;
+ const next = opt.dataset.setStatus;
+ const btn = _activeStatusBtn;
+ const hash = btn.dataset.statusHash;
+ setMirrorStatus(hash, next);
+ btn.textContent = STATUS_CONFIG[next].emoji;
+ btn.title = STATUS_CONFIG[next].label;
+ btn.dataset.currentStatus = next;
+ btn.closest('.mirror-row').style.background = STATUS_CONFIG[next].bg;
+ const ch = App.channels.find(c => c.id === btn.dataset.channelId);
+ if (ch) { const m = ch.mirrors.find(m => m.acestream_hash === hash); if (m) m.status = next; }
+ fetch('/api/mirror/status', {
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ channel_id: btn.dataset.channelId, acestream_hash: hash, status: next }),
+ }).catch(() => {});
+ $('statusPopover').classList.add('hidden');
+ _activeStatusBtn = null;
+ });
+
// ββ Event delegation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// Channel grid click β open modal
@@ -769,6 +898,7 @@
App.activeGroup = null;
App.activeSubcategory = null;
App.activeTags.clear();
+ App.activeCountries.clear();
} else if (btn.dataset.expandGroup !== undefined) {
const g = btn.dataset.expandGroup;
if (App.expandedGroups.has(g)) App.expandedGroups.delete(g);
@@ -786,6 +916,27 @@
renderAll();
});
+ // Country dropdown menu
+ $('countryMenu').addEventListener('click', e => {
+ if (e.target.closest('[data-country-all]')) {
+ App.activeCountries.clear();
+ $('countryMenu').classList.add('hidden');
+ renderAll();
+ return;
+ }
+ if (e.target.closest('[data-country-clear]')) {
+ App.activeCountries.clear();
+ renderAll();
+ return;
+ }
+ const btn = e.target.closest('[data-country]');
+ if (!btn) return;
+ const code = btn.dataset.country;
+ if (App.activeCountries.has(code)) App.activeCountries.delete(code);
+ else App.activeCountries.add(code);
+ renderAll();
+ });
+
// Top bar tag chips
$('topTagsBar').addEventListener('click', e => {
const btn = e.target.closest('[data-top-tag]');
@@ -800,21 +951,7 @@
$('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(() => {});
+ openStatusPopover(statusBtn);
return;
}
const btn = e.target.closest('[data-url]');
@@ -838,21 +975,21 @@
// ββ Dropdown menus βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
function toggleDropdown(which) {
- const menus = { import: $('importMenu'), export: $('exportMenu') };
- const other = which === 'import' ? 'export' : 'import';
- menus[other].classList.add('hidden');
+ const menus = { import: $('importMenu'), export: $('exportMenu'), country: $('countryMenu') };
+ Object.keys(menus).forEach(k => { if (k !== which) menus[k].classList.add('hidden'); });
const menu = menus[which];
- const isHidden = menu.classList.contains('hidden');
- if (isHidden) {
- menu.classList.remove('hidden');
- } else {
- menu.classList.add('hidden');
- }
+ if (menu.classList.contains('hidden')) menu.classList.remove('hidden');
+ else menu.classList.add('hidden');
}
document.addEventListener('click', e => {
- if (!$('importWrap').contains(e.target)) $('importMenu').classList.add('hidden');
- if (!$('exportWrap').contains(e.target)) $('exportMenu').classList.add('hidden');
+ if (!$('importWrap').contains(e.target)) $('importMenu').classList.add('hidden');
+ if (!$('exportWrap').contains(e.target)) $('exportMenu').classList.add('hidden');
+ if (!$('countryWrap').contains(e.target)) $('countryMenu').classList.add('hidden');
+ if (_activeStatusBtn && !$('statusPopover').contains(e.target) && e.target !== _activeStatusBtn) {
+ $('statusPopover').classList.add('hidden');
+ _activeStatusBtn = null;
+ }
});
// ββ Import βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -883,6 +1020,7 @@
App.activeGroup = null;
App.activeSubcategory = null;
App.activeTags.clear();
+ App.activeCountries.clear();
App.expandedGroups.clear();
App.search = '';
$('searchInput').value = '';