jaynesmax commited on
Commit
deaa626
Β·
verified Β·
1 Parent(s): 39a738c

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +178 -119
index.html CHANGED
@@ -117,6 +117,17 @@
117
  .jtv-bar{position:absolute; left:10px; right:10px; top:10px; padding:8px 10px; border-radius:12px; background:rgba(0,0,0,.6); color:#fff; font-size:13px; z-index:5; transition:opacity .3s;}
118
  .jtv-bar.hide{opacity:0; pointer-events:none;}
119
  .jtv-row{display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
 
 
 
 
 
 
 
 
 
 
 
120
  .jtv-live{background:#e5484d; border-radius:6px; padding:2px 7px; font-weight:800; font-size:11px; letter-spacing:.5px;}
121
  .jtv-m{opacity:.8; font-size:12px; margin-top:2px;}
122
  .jtv-b{margin-left:auto; background:rgba(255,255,255,.18); color:#fff; border:0; border-radius:8px; padding:5px 10px; font:inherit; font-size:12px;}
@@ -1215,13 +1226,6 @@
1215
  /* Nafasi ya kusogeza (scroll) ili kipengele chenye focus kisifichwe na topbar / nav */
1216
  .poster,.cw-card,.tab-chip,.settings-row,.dq-item,.tr-ep,.cast-item{scroll-margin:110px 3vw 110px 3vw;}
1217
  </style>
1218
- <style>
1219
- .tr-filter{margin:0 0 10px;}
1220
- .tr-filter .tabs{padding:10px 16px 4px;}
1221
- .tr-filter .tr-dj-row{padding-top:4px;}
1222
- .tr-filter .tr-dj-row .tab-chip{font-size:12px; padding:6px 12px;}
1223
- .tr-filter-note{padding:4px 16px 6px; font-size:12px; color:var(--muted);}
1224
- </style>
1225
  </head>
1226
  <body>
1227
  <div id="gate">
@@ -1492,24 +1496,34 @@ const PIXTVMAX_KEY = SUPABASE_ANON_KEY;
1492
  const TRAILER_BASE = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-trailer';
1493
  const TRAILER_KEY = SUPABASE_ANON_KEY;
1494
 
1495
- // Zilizotafsiriwa: chanzo kimoja (movie-translated-all) β€” n2-, ab-, abm- ids
1496
- const TR_DJ_NOISE = /\b(movies?|filamu|zote|zilizotafsiriwa|zilizotafsiri|imetafsiriwa|iliyotafsiriwa|tafsiriwa|kiswahili|swahili|kwa|mpya|kali|translated|new|all|series|full|single|singo|action|cartoon|katuni|season|seasons|(?:19|20)\d{2})\b/gi;
 
 
 
 
 
 
1497
  function normDj(raw) { // jina safi la DJ (bila maneno ya ziada na bila tofauti za herufi kubwa/ndogo)
1498
  const s = String(raw || '').replace(/\(.*?\)|\[.*?\]/g, ' ').replace(TR_DJ_NOISE, ' ').replace(/[|_\-\u2013\u2014:,.]+/g, ' ').replace(/\s+/g, ' ').trim();
1499
  if (!s || /^dj$/i.test(s)) return '';
1500
- const name = s.split(' ').map(w => /^dj$/i.test(w) ? 'DJ' : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
1501
- return TR_DJ_ALIAS[name.toLowerCase()] || name;
1502
  }
1503
- // Majina tofauti ya DJ mmoja (andika kwa herufi ndogo upande wa kushoto)
1504
- const TR_DJ_ALIAS = { 'dj blaki': 'DJ Black', 'blaki': 'DJ Black', 'black': 'DJ Black' };
1505
  // Model iliyofunzwa (train.py -> HF Model). Ikikosekana, app inafanya kazi kama kawaida (Trending = mpya zaidi).
1506
- const TRANSLATED_ALL = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-translated-all';
1507
  const TR_MODEL_URL = 'https://huggingface.co/jaynesmaxtv/translated-recommender/resolve/main/model.json';
1508
  let trModelP = null;
1509
  function loadTrModel() {
1510
  if (!trModelP) trModelP = fetch(TR_MODEL_URL, { cache: 'no-cache' }).then(r => r.ok ? r.json() : null).catch(() => null);
1511
  return trModelP;
1512
  }
 
 
 
 
 
 
 
 
1513
  // Rails za mlalo: Trending Now (Top 10, kwa model au mpya zaidi) -> New Releases -> kila DJ (DJ Mack kwanza) -> Series -> More to Watch
1514
  function buildTranslatedSections(items, model) {
1515
  const pop = (model && model.popularity) || {}, sections = [], covered = new Set();
@@ -1532,11 +1546,13 @@ function buildTranslatedSections(items, model) {
1532
  if (more.length) sections.push({ title: t('tr_more'), items: more.slice(0, 40) });
1533
  return sections;
1534
  }
1535
- async function fetchTranslatedMovie(id) { // -> { ok, error?, movie } kutoka movie-translated-all
1536
- const r = await fetch(TRANSLATED_ALL + '?id=' + encodeURIComponent(id), { headers: { apikey: SUPABASE_ANON_KEY } });
1537
- const d = await r.json().catch(() => ({}));
1538
- if (!r.ok || !d.ok) return { ok: false, error: d.error || ('HTTP ' + r.status) };
1539
- return d;
 
 
1540
  }
1541
  const PIXTVMAX_CATEGORY_SW = {
1542
  'Sports': 'cat_sports', 'Movie Channels': 'cat_movies', 'News': 'cat_news',
@@ -1552,7 +1568,56 @@ function liveCategoryOf(name) {
1552
  return 'cat_other';
1553
  }
1554
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1555
  async function rpc(op, args = {}) {
 
 
 
 
1556
  const res = await fetch(SUPABASE_FN_URL, {
1557
  method: 'POST',
1558
  headers: { 'Content-Type': 'application/json', 'apikey': SUPABASE_ANON_KEY },
@@ -2885,13 +2950,6 @@ const I18N_EXTRA10 = {
2885
  fr: { jtv_next: 'Ensuite', jtv_schedule: 'Programme', jtv_down: 'JaynesTV est indisponible', tr_trending: 'Tendances actuelles', tr_new: 'NouveautΓ©s', tr_more: 'Γ€ dΓ©couvrir' }
2886
  };
2887
  Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
2888
- const I18N_EXTRA_TRF = {
2889
- sw: { trf_all: 'Zote', trf_new: 'Mpya', trf_trend_movie: 'Movie Zinazovuma', trf_trend_series: 'Series Zinazovuma', trf_movie: 'Movie', trf_series: 'Series', trf_dj: 'Kwa DJ', trf_no_trend: 'Bado hakuna takwimu za kutosha za zinazovuma, tunaonyesha mpya zaidi kwanza.' },
2890
- en: { trf_all: 'All', trf_new: 'New Releases', trf_trend_movie: 'Trending Movies', trf_trend_series: 'Trending Series', trf_movie: 'Movies', trf_series: 'Series', trf_dj: 'By DJ', trf_no_trend: 'Not enough viewing data yet, showing newest first.' },
2891
- ar: { trf_all: 'Ψ§Ω„ΩƒΩ„', trf_new: 'Ψ§Ω„Ψ¬Ψ―ΩŠΨ―', trf_trend_movie: 'أفلام Ψ±Ψ§Ψ¦Ψ¬Ψ©', trf_trend_series: 'Ω…Ψ³Ω„Ψ³Ω„Ψ§Ψͺ Ψ±Ψ§Ψ¦Ψ¬Ψ©', trf_movie: 'أفلام', trf_series: 'Ω…Ψ³Ω„Ψ³Ω„Ψ§Ψͺ', trf_dj: 'Ψ­Ψ³Ψ¨ DJ', trf_no_trend: 'Ω„Ψ§ Ψͺوجد Ψ¨ΩŠΨ§Ω†Ψ§Ψͺ Ω…Ψ΄Ψ§Ω‡Ψ―Ψ© ΩƒΨ§ΩΩŠΨ© بعد، Ω†ΨΉΨ±ΨΆ Ψ§Ω„Ψ£Ψ­Ψ―Ψ« Ψ£ΩˆΩ„Ψ§Ω‹.' },
2892
- fr: { trf_all: 'Tout', trf_new: 'NouveautΓ©s', trf_trend_movie: 'Films tendance', trf_trend_series: 'SΓ©ries tendance', trf_movie: 'Films', trf_series: 'SΓ©ries', trf_dj: 'Par DJ', trf_no_trend: "Pas encore assez de donnΓ©es, les plus rΓ©cents d'abord." }
2893
- };
2894
- Object.keys(I18N_EXTRA_TRF).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA_TRF[l]); });
2895
  function T(k, v) { return t(k, v); }
2896
  // Maandishi yaliyoandikwa kwenye HTML kupitia data-i18n / data-i18n-ph / data-i18n-title
2897
  function applyI18nAttrs() {
@@ -3075,7 +3133,7 @@ function safeImgHTML(urls, alt, lazy){
3075
  * matokeo tayari yamechujwa, tunayabadilisha tu kuwa muundo wa
3076
  * poster card (sawa na toCard()).
3077
  */
3078
- async function searchMovieBox(keyword) {
3079
  const url = SEARCH_FN_URL + '?keyword=' + encodeURIComponent(keyword);
3080
  const res = await fetch(url, { headers: { 'apikey': SUPABASE_ANON_KEY } });
3081
  const arr = await res.json();
@@ -3592,7 +3650,7 @@ async function fetchChannelPosters() {
3592
 
3593
  // Inarudisha orodha bapa (flat) ya channels zote za Live TV β€” inatumika
3594
  // na loadLive() (kuonyesha kwa rails) na search (kuchuja kwa query).
3595
- // ── JAYNESTV: live TV ya kubuni kutoka video za dataset yako (ratiba: build_jaynestv.py) ──
3596
  const JTV_URL = 'https://huggingface.co/datasets/jaynesmaxtv/movie-file/resolve/main/jaynestv/schedule.json';
3597
  let jtvP = null, jtvAt = 0;
3598
  function jtvSchedule(force) { // ratiba inahifadhiwa dakika 5; ikikosekana JaynesTV haionekani
@@ -3610,23 +3668,76 @@ function jtvNow(s, nowMs) { // kipindi cha sasa + vinavyofuata, kwa saa: kila mt
3610
  for (let k = 1; k <= 8; k++) { const it = s.items[(i + k) % s.items.length]; upcoming.push({ item: it, at }); at += it.dur * 1000; }
3611
  return { idx: i, item: s.items[i], offset: pos, left: s.items[i].dur - pos, upcoming };
3612
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3613
  async function openJaynesTV() {
3614
  openPlayerShell('JaynesTV');
3615
  const s = await jtvSchedule(true);
3616
  if (!s) return statusMsg('<span style="color:#ffb4ab">' + esc(T('jtv_down')) + '</span>');
 
 
3617
  stage.style.position = 'relative';
3618
  stage.innerHTML = '<video id="vid" controls autoplay playsinline></video><div class="jtv-bar" id="jtvBar"></div>';
3619
  const v = document.getElementById('vid'), bar = document.getElementById('jtvBar');
3620
  attachPlayerEnhancements(v); ensureSound(v);
 
 
 
3621
  const bad = new Set(); let cur = null, epg = false, hideT = null, last = null;
3622
  const hhmm = ms => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
3623
  const show = (ms) => { bar.classList.remove('hide'); clearTimeout(hideT); if (ms) hideT = setTimeout(() => { if (!epg) bar.classList.add('hide'); }, ms); };
3624
  const draw = (n, it) => {
3625
- const nx = n.upcoming[0];
3626
- bar.innerHTML = '<div class="jtv-row"><span class="jtv-live">● LIVE</span><b>' + esc(it.title) + '</b>' + (it.dj ? '<span class="jtv-m">' + esc(it.dj) + '</span>' : '') +
3627
- '<button class="jtv-b" data-j="epg">' + esc(T('jtv_schedule')) + '</button></div>' +
3628
  '<div class="jtv-m">' + esc(T('jtv_next')) + ': ' + esc(nx.item.title) + ' Β· ' + hhmm(nx.at) + '</div>' +
3629
- (epg ? '<div class="jtv-epg">' + n.upcoming.map(u => '<div><span>' + hhmm(u.at) + '</span> ' + esc(u.item.title) + '</div>').join('') + '</div>' : '');
3630
  };
3631
  const sync = () => {
3632
  const n = jtvNow(s, Date.now()), isBad = bad.has(n.idx);
@@ -3634,7 +3745,7 @@ async function openJaynesTV() {
3634
  if (cur !== it.url) {
3635
  cur = it.url; v.src = it.url + '#t=' + Math.floor(off);
3636
  v.addEventListener('loadedmetadata', () => { if (Math.abs(v.currentTime - off) > 3) v.currentTime = off; const p = v.play(); if (p && p.catch) p.catch(() => {}); }, { once: true });
3637
- show(8000);
3638
  } else if (!isBad && v.readyState > 1 && Math.abs(v.currentTime - off) > 4) v.currentTime = off; // rudi kwenye 'live'
3639
  last = n; draw(n, it);
3640
  };
@@ -3645,18 +3756,22 @@ async function openJaynesTV() {
3645
  v.addEventListener('error', () => { if (last) { bad.add(last.idx); sync(); } });
3646
  v.addEventListener('click', () => show(6000));
3647
  const timer = setInterval(sync, 15000);
3648
- window.__jtvStop = () => { clearInterval(timer); clearTimeout(hideT); };
 
3649
  sync();
3650
  }
3651
 
3652
- async function fetchAllLiveItems() {
3653
  const items = [];
3654
  let anyOk = false, lastErr = null;
3655
  const posters = await fetchChannelPosters();
3656
 
3657
  try { // JaynesTV (ikiwa ratiba ipo): kadi inaonyesha kipindi kinachochezwa sasa
3658
  const js = await jtvSchedule();
3659
- if (js) { const n = jtvNow(js, Date.now()); items.push({ id: 'jaynestv', kind: 'live', title: n.item.title, poster: n.item.poster || js.logo || null, subtitle: 'JaynesTV', imgFallbackOn: true }); }
 
 
 
3660
  } catch (_) {}
3661
 
3662
  try {
@@ -3713,83 +3828,30 @@ async function loadLive() {
3713
  }
3714
 
3715
  // ── ZILIZOTAFSIRIWA (movies/series za DJ β€” mradi tofauti) ──
3716
- async function fetchTranslatedList() {
3717
  const card = (m, idPrefix) => ({
3718
  id: idPrefix + m.id, kind: 'translated', title: m.title,
3719
  poster: m.thumb_url || m.poster_url || null, imgFallbackOn: true,
3720
  subtitle: normDj(m.dj) || (m.category === 'series' ? 'Series' : 'Movie'), isSeries: m.category === 'series',
3721
- added: m.created_at || m.updated_at || '',
3722
- });
3723
- const r = await fetch(TRANSLATED_ALL, { headers: { apikey: SUPABASE_ANON_KEY } });
3724
- if (!r.ok) throw new Error(T('err_generic') + ' ' + r.status);
3725
- const d = await r.json();
3726
- if (!d.ok) throw new Error(d.error || 'Imekataa ombi.');
3727
- return (d.movies || []).map(m => card(m, ''));
3728
- }
3729
-
3730
- // ── KUPANGA ZILIZOTAFSIRIWA: Zote / Mpya / Movie Zinazovuma / Series Zinazovuma / Movie / Series / Kwa DJ ──
3731
- let trCache = null; // { items, model } -- kubadilisha kichujio hakupakui upya
3732
- let trFilter = 'all', trDj = '';
3733
- try { trFilter = sessionStorage.getItem('jm_tr_filter') || 'all'; trDj = sessionStorage.getItem('jm_tr_dj') || ''; } catch (_) {}
3734
- const TR_FILTERS = ['all', 'new', 'trend_movie', 'trend_series', 'movie', 'series', 'dj'];
3735
- function trDjGroups(items) { // DJ Mack kwanza, kisha DJ mwenye kazi nyingi zaidi
3736
- const g = {};
3737
- items.forEach(it => {
3738
- const dj = String(it.subtitle || '').trim();
3739
- if (!dj || dj === 'Series' || dj === 'Movie') return;
3740
- (g[dj.toLowerCase()] = g[dj.toLowerCase()] || { name: dj, list: [] }).list.push(it);
3741
  });
3742
- return Object.values(g).sort((a, b) => (/mack/i.test(b.name) - /mack/i.test(a.name)) || b.list.length - a.list.length || a.name.localeCompare(b.name));
3743
- }
3744
- function trByNewest(list) { // mpya -> zamani kwa tarehe ya kuongezwa; zisizo na tarehe zinabaki kwa mpangilio wa seva
3745
- return list.map((it, i) => [it, i])
3746
- .sort((a, b) => (Date.parse(b[0].added) || 0) - (Date.parse(a[0].added) || 0) || a[1] - b[1]).map(x => x[0]);
3747
- }
3748
- function trTrending(list, model) { // kwa uchezaji halisi (model); zikipungua, zinaongezwa mpya zaidi bila namba
3749
- const pop = (model && model.popularity) || {};
3750
- const hot = list.filter(it => pop[it.id] > 0).sort((a, b) => pop[b.id] - pop[a.id]);
3751
- const ranked = hot.slice(0, 30).map((it, i) => ({ ...it, rank: i < 10 ? i + 1 : undefined }));
3752
- if (ranked.length < 10) {
3753
- const have = new Set(ranked.map(it => it.id));
3754
- trByNewest(list).forEach(it => { if (ranked.length < 30 && !have.has(it.id)) ranked.push(it); });
3755
- }
3756
- return { items: ranked, fallback: hot.length === 0 };
3757
- }
3758
- function renderTranslatedView() {
3759
- if (!trCache) return;
3760
- const { items, model } = trCache;
3761
- const movies = items.filter(it => !it.isSeries), series = items.filter(it => it.isSeries);
3762
- const groups = trDjGroups(items);
3763
- if (!TR_FILTERS.includes(trFilter)) trFilter = 'all';
3764
- if (trFilter === 'dj' && !groups.some(g => g.name === trDj)) trDj = groups[0] ? groups[0].name : '';
3765
- let sections, grid = true, note = '';
3766
- if (trFilter === 'new') sections = [{ title: t('tr_new'), items: trByNewest(items) }];
3767
- else if (trFilter === 'trend_movie' || trFilter === 'trend_series') {
3768
- const r = trTrending(trFilter === 'trend_movie' ? movies : series, model);
3769
- if (r.fallback) note = t('trf_no_trend');
3770
- sections = [{ title: t('trf_' + trFilter), items: r.items }];
3771
- }
3772
- else if (trFilter === 'movie') sections = [{ title: t('trf_movie'), items: trByNewest(movies) }];
3773
- else if (trFilter === 'series') sections = [{ title: t('trf_series'), items: trByNewest(series) }];
3774
- else if (trFilter === 'dj') {
3775
- const g = groups.find(x => x.name === trDj);
3776
- sections = g ? [{ title: g.name, items: trByNewest(g.list).map(it => ({ ...it, subtitle: it.isSeries ? 'Series' : '' })) }] : [];
3777
- }
3778
- else { sections = buildTranslatedSections(items, model); grid = false; }
3779
- renderRails(sections, grid);
3780
- const bar = document.createElement('div');
3781
- bar.className = 'tr-filter';
3782
- bar.innerHTML = `<div class="tabs">${TR_FILTERS.map(f => `<div class="tab-chip${f === trFilter ? ' active' : ''}" data-trf="${f}">${esc(t('trf_' + f))}</div>`).join('')}</div>` +
3783
- (trFilter === 'dj' && groups.length ? `<div class="tabs tr-dj-row">${groups.map(g => `<div class="tab-chip${g.name === trDj ? ' active' : ''}" data-trdj="${esc(g.name)}">${esc(g.name)} Β· ${g.list.length}</div>`).join('')}</div>` : '') +
3784
- (note ? `<div class="tr-filter-note">${esc(note)}</div>` : '');
3785
- bar.onclick = (e) => {
3786
- const c = e.target.closest('.tab-chip'); if (!c) return;
3787
- if (c.dataset.trf) trFilter = c.dataset.trf; else if (c.dataset.trdj != null) trDj = c.dataset.trdj; else return;
3788
- try { sessionStorage.setItem('jm_tr_filter', trFilter); sessionStorage.setItem('jm_tr_dj', trDj); } catch (_) {}
3789
- renderTranslatedView();
3790
- };
3791
- railsEl.insertBefore(bar, railsEl.firstChild);
3792
- bar.querySelectorAll('.tab-chip.active').forEach(c => { const row = c.parentElement; row.scrollLeft = c.offsetLeft - (row.clientWidth - c.offsetWidth) / 2; });
3793
  }
3794
 
3795
  async function loadTranslated() {
@@ -3797,8 +3859,7 @@ async function loadTranslated() {
3797
  renderSkeletonRails(2);
3798
  try {
3799
  const [items, model] = await Promise.all([fetchTranslatedList(), loadTrModel()]);
3800
- trCache = { items, model };
3801
- renderTranslatedView();
3802
  } catch (e) {
3803
  railsEl.innerHTML = '';
3804
  railsEl.appendChild(stateBox(t('err_failed', { e: esc(e.message || t('err_generic')) }), true, loadTranslated));
@@ -4030,6 +4091,7 @@ function playTranslatedUrl(url, title, playlist, meta) {
4030
  v.addEventListener('ended', () => { const c = window.__pcPlaylist; if (c && typeof c.next === 'function') c.next(); });
4031
  // Tangazo la video kwanza (likishindikana/likiruka, filamu inaanza mara moja)
4032
  v.src = url;
 
4033
  // Route ya player: #/translated/ID/play?se=..&ep=..
4034
  try {
4035
  const id = currentTranslatedDetail && currentTranslatedDetail.id;
@@ -4130,7 +4192,7 @@ async function shareTranslatedTitle(d) {
4130
  // ── MICHEZO (fawa-live) ──
4131
  // Inarudisha orodha bapa (flat) ya mechi zote β€” inatumika na loadSports()
4132
  // (kuonyesha kwa rails, kikundi kwa ligi) na search (kuchuja kwa query).
4133
- async function fetchAllSportsItems() {
4134
  const res = await fetch(FAWA_BASE + '/matches', { headers: { Authorization: 'Bearer ' + FAWA_KEY } });
4135
  if (!res.ok) throw new Error(T('err_generic') + ' ' + res.status);
4136
  const d = await res.json();
@@ -6935,11 +6997,7 @@ async function loadTrailer(subjectId){
6935
  if (!wrap) return;
6936
  wrap.innerHTML = '<div class="trailer-state">' + T('trailer_loading') + '</div>';
6937
  try {
6938
- const res = await fetch(TRAILER_BASE + '?subjectId=' + encodeURIComponent(subjectId), {
6939
- headers: { apikey: TRAILER_KEY, Authorization: 'Bearer ' + TRAILER_KEY },
6940
- });
6941
- if (!res.ok) throw new Error('http_' + res.status);
6942
- const result = await res.json();
6943
  if (result.code !== 0) throw new Error(result.message || 'trailer_error');
6944
  // Kagua kila hatua kabla ya kutumia β€” data.trailer, .url zote zinaweza kukosekana
6945
  const trailer = result.data && result.data.trailer;
@@ -10008,8 +10066,9 @@ async function muxJob(videoUrl, subtitles, filename, onProgress){
10008
  if (!startRes.ok || !startData.jobId) throw new Error(startData.error || t('dq_err_mux_start'));
10009
  const jobId = startData.jobId;
10010
 
10011
- for (let tries = 0; tries < 180; tries++) { // hadi ~15 dakika (5s * 180)
10012
- await new Promise(r => setTimeout(r, 5000));
 
10013
  const st = await fetch(MUX_BASE + '?jobId=' + encodeURIComponent(jobId), {
10014
  headers: { 'Authorization': 'Bearer ' + MUX_KEY, 'apikey': MUX_KEY },
10015
  });
@@ -10606,7 +10665,7 @@ function nhSliderRender(box, sl){
10606
  }
10607
  }
10608
  // Config ya Home inatoka Supabase (home-config); ikishindwa tunatumia nakala ya mwisho au chaguo-msingi
10609
- async function getHomeConfig(){
10610
  try {
10611
  const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 3000);
10612
  const r = await fetch(HOME_CFG_URL, { method: 'POST', signal: ctl.signal, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + SUPABASE_ANON_KEY, apikey: SUPABASE_ANON_KEY }, body: JSON.stringify({ action: 'get' }) });
 
117
  .jtv-bar{position:absolute; left:10px; right:10px; top:10px; padding:8px 10px; border-radius:12px; background:rgba(0,0,0,.6); color:#fff; font-size:13px; z-index:5; transition:opacity .3s;}
118
  .jtv-bar.hide{opacity:0; pointer-events:none;}
119
  .jtv-row{display:flex; gap:8px; align-items:center; flex-wrap:wrap;}
120
+ .jtv-bug{position:absolute; z-index:4; pointer-events:none; width:auto; max-width:30%; object-fit:contain; filter:drop-shadow(0 1px 4px rgba(0,0,0,.6));}
121
+ .jtv-top-right{top:12px; right:12px;} .jtv-top-left{top:12px; left:12px;} .jtv-bottom-right{bottom:64px; right:12px;} .jtv-bottom-left{bottom:64px; left:12px;}
122
+ .jtv-ident{position:absolute; inset:0; z-index:6; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px; background:radial-gradient(circle at 50% 40%,#1b1640,#07070f); color:#fff; pointer-events:none; transition:opacity .6s;}
123
+ .jtv-ident img{height:34%; max-height:180px; width:auto; animation:jtvPop .9s ease-out;} .jtv-ident.out{opacity:0;}
124
+ .jtv-idn{font-weight:900; font-size:22px; letter-spacing:2px;} .jtv-idt{opacity:.75; font-size:13px;}
125
+ @keyframes jtvPop{from{transform:scale(.7); opacity:0;} to{transform:scale(1); opacity:1;}}
126
+ .jtv-lt{position:absolute; left:10px; bottom:64px; z-index:5; display:flex; gap:8px; align-items:center; background:rgba(0,0,0,.65); color:#fff; padding:6px 10px; border-radius:10px; font-size:13px; pointer-events:none; animation:jtvPop .5s ease-out;}
127
+ .jtv-ltk{border-radius:5px; padding:1px 6px; font-size:11px; font-weight:800; letter-spacing:.5px;}
128
+ .jtv-ticker{position:absolute; left:0; right:0; bottom:44px; z-index:4; overflow:hidden; background:rgba(0,0,0,.55); color:#fff; font-size:13px; padding:4px 0; pointer-events:none;}
129
+ .jtv-tk{display:inline-block; white-space:nowrap; padding-left:100%; animation:jtvTk linear infinite;}
130
+ @keyframes jtvTk{from{transform:translateX(0);} to{transform:translateX(-100%);}}
131
  .jtv-live{background:#e5484d; border-radius:6px; padding:2px 7px; font-weight:800; font-size:11px; letter-spacing:.5px;}
132
  .jtv-m{opacity:.8; font-size:12px; margin-top:2px;}
133
  .jtv-b{margin-left:auto; background:rgba(255,255,255,.18); color:#fff; border:0; border-radius:8px; padding:5px 10px; font:inherit; font-size:12px;}
 
1226
  /* Nafasi ya kusogeza (scroll) ili kipengele chenye focus kisifichwe na topbar / nav */
1227
  .poster,.cw-card,.tab-chip,.settings-row,.dq-item,.tr-ep,.cast-item{scroll-margin:110px 3vw 110px 3vw;}
1228
  </style>
 
 
 
 
 
 
 
1229
  </head>
1230
  <body>
1231
  <div id="gate">
 
1496
  const TRAILER_BASE = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-trailer';
1497
  const TRAILER_KEY = SUPABASE_ANON_KEY;
1498
 
1499
+ // movie-zilizotafsiriwa β€” mradi TOFAUTI (DJ movies/series), function ya umma pekee (GET)
1500
+ const TRANSLATED_BASE = JM_CONFIG.TRANSLATED_URL + '/functions/v1/' + JM_CONFIG.TRANSLATED_FN;
1501
+ const TRANSLATED_KEY = JM_CONFIG.TRANSLATED_KEY;
1502
+ // movie-zilizotafsriwa2 β€” filamu zenye MP4 (draft/public kutoka admin); project ile ile ya CONTENT_URL.
1503
+ // Id zake zinaanza na 'n2-' ili zisigongane na za chanzo cha zamani.
1504
+ const TRANSLATED2_BASE = JM_CONFIG.CONTENT_URL + '/functions/v1/movie-zilizotafsriwa2';
1505
+ const TR2_PREFIX = 'n2-';
1506
+ const TR_DJ_NOISE = /\b(movies?|filamu|zote|zilizotafsiriwa|zilizotafsiri|imetafsiriwa|iliyotafsiriwa|tafsiriwa|kiswahili|swahili|kwa|mpya|kali|translated|new|all|series|full)\b/gi;
1507
  function normDj(raw) { // jina safi la DJ (bila maneno ya ziada na bila tofauti za herufi kubwa/ndogo)
1508
  const s = String(raw || '').replace(/\(.*?\)|\[.*?\]/g, ' ').replace(TR_DJ_NOISE, ' ').replace(/[|_\-\u2013\u2014:,.]+/g, ' ').replace(/\s+/g, ' ').trim();
1509
  if (!s || /^dj$/i.test(s)) return '';
1510
+ return s.split(' ').map(w => /^dj$/i.test(w) ? 'DJ' : w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' ');
 
1511
  }
 
 
1512
  // Model iliyofunzwa (train.py -> HF Model). Ikikosekana, app inafanya kazi kama kawaida (Trending = mpya zaidi).
 
1513
  const TR_MODEL_URL = 'https://huggingface.co/jaynesmaxtv/translated-recommender/resolve/main/model.json';
1514
  let trModelP = null;
1515
  function loadTrModel() {
1516
  if (!trModelP) trModelP = fetch(TR_MODEL_URL, { cache: 'no-cache' }).then(r => r.ok ? r.json() : null).catch(() => null);
1517
  return trModelP;
1518
  }
1519
+ function logTrPlay(id) { // rekodi uchezaji bila kuzuia player; kitambulisho cha kifaa ni cha nasibu (hakuna jina/email)
1520
+ if (!id) return;
1521
+ let dev = '';
1522
+ try { dev = localStorage.getItem('jm_dev') || ''; if (!dev) { dev = (crypto.randomUUID && crypto.randomUUID()) || (Date.now() + '-' + Math.random().toString(36).slice(2)); localStorage.setItem('jm_dev', dev); } } catch (_) {}
1523
+ fetch(TRANSLATED2_BASE + '?action=play', { method: 'POST', keepalive: true,
1524
+ headers: { apikey: SUPABASE_ANON_KEY, 'Content-Type': 'application/json' },
1525
+ body: JSON.stringify({ movie_id: String(id), device_id: dev }) }).catch(() => {});
1526
+ }
1527
  // Rails za mlalo: Trending Now (Top 10, kwa model au mpya zaidi) -> New Releases -> kila DJ (DJ Mack kwanza) -> Series -> More to Watch
1528
  function buildTranslatedSections(items, model) {
1529
  const pop = (model && model.popularity) || {}, sections = [], covered = new Set();
 
1546
  if (more.length) sections.push({ title: t('tr_more'), items: more.slice(0, 40) });
1547
  return sections;
1548
  }
1549
+ async function fetchTranslatedMovie(id) { // -> { ok, error?, movie } kutoka chanzo sahihi
1550
+ const isNew = String(id).startsWith(TR2_PREFIX);
1551
+ const url = isNew ? TRANSLATED2_BASE + '?id=' + encodeURIComponent(String(id).slice(TR2_PREFIX.length))
1552
+ : TRANSLATED_BASE + '?id=' + encodeURIComponent(id);
1553
+ const r = await fetch(url, { headers: { apikey: isNew ? SUPABASE_ANON_KEY : TRANSLATED_KEY } });
1554
+ const d = await r.json();
1555
+ return isNew ? { ok: r.ok && !d.error, error: d.error, movie: d } : d;
1556
  }
1557
  const PIXTVMAX_CATEGORY_SW = {
1558
  'Sports': 'cat_sports', 'Movie Channels': 'cat_movies', 'News': 'cat_news',
 
1568
  return 'cat_other';
1569
  }
1570
 
1571
+ // ── CACHE ya majibu ya Supabase functions (inapunguza invocations): kumbukumbu + localStorage, ombi moja kwa wote wanaoomba kwa pamoja,
1572
+ // na nakala ya zamani ikiwa seva imeshindwa. Majibu yenye hitilafu hayahifadhiwi. ──
1573
+ const JM_MEM = new Map(), JM_INFLIGHT = new Map();
1574
+ const jmClone = v => { try { return structuredClone(v); } catch (_) { return JSON.parse(JSON.stringify(v)); } };
1575
+ async function jmCached(key, ttl, fn, opt = {}) {
1576
+ const persist = opt.persist !== false, ok = opt.ok || (v => v != null);
1577
+ let hit = JM_MEM.get(key);
1578
+ if (!hit && persist) { try { const j = JSON.parse(localStorage.getItem('jmc:' + key)); if (j && j.t) { hit = j; JM_MEM.set(key, j); } } catch (_) {} }
1579
+ if (hit && Date.now() - hit.t < ttl) return jmClone(hit.v);
1580
+ if (JM_INFLIGHT.has(key)) return jmClone(await JM_INFLIGHT.get(key));
1581
+ const p = (async () => {
1582
+ try {
1583
+ const v = await fn();
1584
+ if (ok(v)) {
1585
+ const rec = { t: Date.now(), v: jmClone(v) }; JM_MEM.set(key, rec);
1586
+ if (persist) { try { const sj = JSON.stringify(rec); if (sj.length < 300000) localStorage.setItem('jmc:' + key, sj); } catch (_) {} }
1587
+ }
1588
+ return v;
1589
+ } catch (e) { if (hit) return hit.v; throw e; } // seva imeshindwa: tumia nakala ya zamani kuliko kuonyesha kosa
1590
+ finally { JM_INFLIGHT.delete(key); }
1591
+ })();
1592
+ JM_INFLIGHT.set(key, p);
1593
+ return jmClone(await p);
1594
+ }
1595
+ try { for (let i = localStorage.length - 1; i >= 0; i--) { const k = localStorage.key(i); if (k && k.startsWith('jmc:')) { const j = JSON.parse(localStorage.getItem(k) || 'null'); if (!j || Date.now() - j.t > 2 * 86400000) localStorage.removeItem(k); } } } catch (_) {}
1596
+ const jmOkObj = r => r && typeof r === 'object' && !r.error && r.success !== false;
1597
+ const RPC_CACHE = { // op -> [ttl ms, hifadhi localStorage?]; movies.play haihifadhiwi kamwe (viungo vya kucheza)
1598
+ 'movies.home': [600000, true], 'movies.tabs': [1800000, true], 'movies.tab': [600000, true],
1599
+ 'movies.related': [1800000, false], 'movies.series': [600000, false], 'movies.seasons': [600000, false], 'movies.episodes': [600000, false],
1600
+ 'movies.staffInfo': [3600000, false], 'movies.staffSubjectList': [3600000, false],
1601
+ };
1602
+ // Orodha zinazotumika na search, Home na tabs: ombi moja la mtandao kwa dakika chache badala ya kila mara
1603
+ function fetchAllLiveItems() { return jmCached('live', 300000, fetchAllLiveItemsRaw, { persist: false }); }
1604
+ function fetchAllSportsItems() { return jmCached('sports', 120000, fetchAllSportsItemsRaw, { persist: false }); }
1605
+ function fetchTranslatedList() { return jmCached('translated', 300000, fetchTranslatedListRaw, { persist: false }); }
1606
+ function searchMovieBox(keyword) { return jmCached('search:' + String(keyword).trim().toLowerCase(), 600000, () => searchMovieBoxRaw(keyword), { persist: false }); }
1607
+ function getHomeConfig() { return jmCached('homecfg', 600000, getHomeConfigRaw, { persist: false, ok: v => v && v !== HOME_CFG_DEFAULT }); }
1608
+ async function fetchTrailerJson(subjectId) {
1609
+ return jmCached('trailer:' + subjectId, 86400000, async () => {
1610
+ const res = await fetch(TRAILER_BASE + '?subjectId=' + encodeURIComponent(subjectId), { headers: { apikey: TRAILER_KEY, Authorization: 'Bearer ' + TRAILER_KEY } });
1611
+ if (!res.ok) throw new Error('http_' + res.status);
1612
+ return res.json();
1613
+ }, { persist: false, ok: r => r && r.code === 0 });
1614
+ }
1615
+
1616
  async function rpc(op, args = {}) {
1617
+ const c = RPC_CACHE[op];
1618
+ return c ? jmCached('rpc:' + op + ':' + JSON.stringify(args), c[0], () => rpcRaw(op, args), { persist: c[1], ok: jmOkObj }) : rpcRaw(op, args);
1619
+ }
1620
+ async function rpcRaw(op, args = {}) {
1621
  const res = await fetch(SUPABASE_FN_URL, {
1622
  method: 'POST',
1623
  headers: { 'Content-Type': 'application/json', 'apikey': SUPABASE_ANON_KEY },
 
2950
  fr: { jtv_next: 'Ensuite', jtv_schedule: 'Programme', jtv_down: 'JaynesTV est indisponible', tr_trending: 'Tendances actuelles', tr_new: 'NouveautΓ©s', tr_more: 'Γ€ dΓ©couvrir' }
2951
  };
2952
  Object.keys(I18N_EXTRA10).forEach(l => { I18N[l] = Object.assign(I18N[l] || {}, I18N_EXTRA10[l]); });
 
 
 
 
 
 
 
2953
  function T(k, v) { return t(k, v); }
2954
  // Maandishi yaliyoandikwa kwenye HTML kupitia data-i18n / data-i18n-ph / data-i18n-title
2955
  function applyI18nAttrs() {
 
3133
  * matokeo tayari yamechujwa, tunayabadilisha tu kuwa muundo wa
3134
  * poster card (sawa na toCard()).
3135
  */
3136
+ async function searchMovieBoxRaw(keyword) {
3137
  const url = SEARCH_FN_URL + '?keyword=' + encodeURIComponent(keyword);
3138
  const res = await fetch(url, { headers: { 'apikey': SUPABASE_ANON_KEY } });
3139
  const arr = await res.json();
 
3650
 
3651
  // Inarudisha orodha bapa (flat) ya channels zote za Live TV β€” inatumika
3652
  // na loadLive() (kuonyesha kwa rails) na search (kuchuja kwa query).
3653
+ // ── JAYNESTV: TV yako (jina, nembo, rangi na vipengele vinatoka kwenye jaynestv/config.json; ratiba: build_jaynestv.py) ──
3654
  const JTV_URL = 'https://huggingface.co/datasets/jaynesmaxtv/movie-file/resolve/main/jaynestv/schedule.json';
3655
  let jtvP = null, jtvAt = 0;
3656
  function jtvSchedule(force) { // ratiba inahifadhiwa dakika 5; ikikosekana JaynesTV haionekani
 
3668
  for (let k = 1; k <= 8; k++) { const it = s.items[(i + k) % s.items.length]; upcoming.push({ item: it, at }); at += it.dur * 1000; }
3669
  return { idx: i, item: s.items[i], offset: pos, left: s.items[i].dur - pos, upcoming };
3670
  }
3671
+ const JTV_DEFAULTS = { name: 'JaynesTV', tagline: '', logo: '', accent: '#7c5cff', card: 'logo',
3672
+ features: { bug: true, ident: true, lowerthird: true, ticker: false, epg: true },
3673
+ bug: { position: 'top-right', opacity: 0.85, size: 54 }, ticker: { messages: [], speed: 60 } };
3674
+ function jtvCfg(s) { // chaguo-msingi + config yako (funguo zisizojulikana zinapita ili vipengele vipya vizitumie)
3675
+ const c = (s && s.config) || {}, m = (a, b) => Object.assign({}, a, b || {});
3676
+ const cfg = Object.assign({}, JTV_DEFAULTS, c, { features: m(JTV_DEFAULTS.features, c.features), bug: m(JTV_DEFAULTS.bug, c.bug), ticker: m(JTV_DEFAULTS.ticker, c.ticker) });
3677
+ if (!/^#[0-9a-f]{3,8}$/i.test(String(cfg.accent))) cfg.accent = JTV_DEFAULTS.accent;
3678
+ if (!/^https:\/\//.test(String(cfg.logo || ''))) cfg.logo = '';
3679
+ return cfg;
3680
+ }
3681
+ // ── MFUMO WA VIPENGELE (plugins): kuongeza kipengele kipya andika jtvFeature('jina', { open(c){}, program(c,n,it){}, close(c){} })
3682
+ // kisha kiwashe kwenye config.json: "features": { "jina": true }. 'c' ina: stage, v (video), bar, s (ratiba), cfg, own[] (vipengele vya DOM
3683
+ // vitakavyoondolewa kiotomatiki), timers[] (vitafutwa kiotomatiki). Kipengele chenye hitilafu hakiangushi TV.
3684
+ const JTV_FEATURES = {};
3685
+ function jtvFeature(name, def) { JTV_FEATURES[name] = def; }
3686
+ const jtvEl = (cls, html) => { const d = document.createElement('div'); d.className = cls; if (html) d.innerHTML = html; return d; };
3687
+ jtvFeature('bug', { // nembo ndogo kwenye kona ya video
3688
+ open(c) {
3689
+ if (!c.cfg.logo) return;
3690
+ const b = document.createElement('img'); b.className = 'jtv-bug jtv-' + c.cfg.bug.position; b.src = c.cfg.logo; b.alt = '';
3691
+ b.style.opacity = String(c.cfg.bug.opacity); b.style.height = (+c.cfg.bug.size || 54) + 'px';
3692
+ c.stage.appendChild(b); c.own.push(b);
3693
+ },
3694
+ });
3695
+ jtvFeature('ident', { // skrini ya utambulisho (nembo + jina + kauli mbiu) mwanzoni
3696
+ open(c) {
3697
+ const d = jtvEl('jtv-ident', (c.cfg.logo ? '<img src="' + esc(c.cfg.logo) + '" alt="">' : '') + '<div class="jtv-idn">' + esc(c.cfg.name) + '</div>' +
3698
+ (c.cfg.tagline ? '<div class="jtv-idt">' + esc(c.cfg.tagline) + '</div>' : ''));
3699
+ c.stage.appendChild(d); c.own.push(d);
3700
+ const t0 = Date.now(), done = () => { d.classList.add('out'); c.timers.push(setTimeout(() => d.remove(), 600)); };
3701
+ c.v.addEventListener('playing', () => c.timers.push(setTimeout(done, Math.max(0, 1500 - (Date.now() - t0)))), { once: true });
3702
+ c.timers.push(setTimeout(done, 4000));
3703
+ },
3704
+ });
3705
+ jtvFeature('lowerthird', { // "NOW: jina la filamu" kipindi kinapoanza
3706
+ program(c, n, it) {
3707
+ const d = jtvEl('jtv-lt', '<span class="jtv-ltk" style="background:' + esc(c.cfg.accent) + '">NOW</span><b>' + esc(it.title) + '</b>' + (it.dj ? ' <span class="jtv-m">' + esc(it.dj) + '</span>' : ''));
3708
+ c.stage.appendChild(d); c.own.push(d); c.timers.push(setTimeout(() => d.remove(), 7000));
3709
+ },
3710
+ });
3711
+ jtvFeature('ticker', { // maandishi yanayopita chini (config.ticker.messages)
3712
+ open(c) {
3713
+ const msgs = (c.cfg.ticker.messages || []).map(String).filter(Boolean); if (!msgs.length) return;
3714
+ const text = msgs.join(' β€’ '), dur = Math.max(12, text.length / (Math.max(10, +c.cfg.ticker.speed || 60) / 10));
3715
+ const d = jtvEl('jtv-ticker', '<div class="jtv-tk" style="animation-duration:' + dur.toFixed(0) + 's">' + esc(text) + '</div>');
3716
+ c.stage.appendChild(d); c.own.push(d);
3717
+ },
3718
+ });
3719
  async function openJaynesTV() {
3720
  openPlayerShell('JaynesTV');
3721
  const s = await jtvSchedule(true);
3722
  if (!s) return statusMsg('<span style="color:#ffb4ab">' + esc(T('jtv_down')) + '</span>');
3723
+ const cfg = jtvCfg(s);
3724
+ try { ptitle.textContent = cfg.name; } catch (_) {}
3725
  stage.style.position = 'relative';
3726
  stage.innerHTML = '<video id="vid" controls autoplay playsinline></video><div class="jtv-bar" id="jtvBar"></div>';
3727
  const v = document.getElementById('vid'), bar = document.getElementById('jtvBar');
3728
  attachPlayerEnhancements(v); ensureSound(v);
3729
+ const ctx = { stage, v, bar, s, cfg, own: [], timers: [] };
3730
+ const on = name => cfg.features[name] === true || (cfg.features[name] !== false && name in JTV_DEFAULTS.features && JTV_DEFAULTS.features[name]);
3731
+ const hook = (h, ...a) => Object.keys(JTV_FEATURES).forEach(k => { if (on(k) && JTV_FEATURES[k][h]) { try { JTV_FEATURES[k][h](ctx, ...a); } catch (e) { console.warn('jtv feature', k, e); } } });
3732
  const bad = new Set(); let cur = null, epg = false, hideT = null, last = null;
3733
  const hhmm = ms => new Date(ms).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
3734
  const show = (ms) => { bar.classList.remove('hide'); clearTimeout(hideT); if (ms) hideT = setTimeout(() => { if (!epg) bar.classList.add('hide'); }, ms); };
3735
  const draw = (n, it) => {
3736
+ const nx = n.upcoming[0], canEpg = on('epg');
3737
+ bar.innerHTML = '<div class="jtv-row"><span class="jtv-live" style="background:' + esc(cfg.accent) + '">● LIVE</span><b>' + esc(it.title) + '</b>' + (it.dj ? '<span class="jtv-m">' + esc(it.dj) + '</span>' : '') +
3738
+ (canEpg ? '<button class="jtv-b" data-j="epg">' + esc(T('jtv_schedule')) + '</button>' : '') + '</div>' +
3739
  '<div class="jtv-m">' + esc(T('jtv_next')) + ': ' + esc(nx.item.title) + ' Β· ' + hhmm(nx.at) + '</div>' +
3740
+ (epg && canEpg ? '<div class="jtv-epg">' + n.upcoming.map(u => '<div><span>' + hhmm(u.at) + '</span> ' + esc(u.item.title) + '</div>').join('') + '</div>' : '');
3741
  };
3742
  const sync = () => {
3743
  const n = jtvNow(s, Date.now()), isBad = bad.has(n.idx);
 
3745
  if (cur !== it.url) {
3746
  cur = it.url; v.src = it.url + '#t=' + Math.floor(off);
3747
  v.addEventListener('loadedmetadata', () => { if (Math.abs(v.currentTime - off) > 3) v.currentTime = off; const p = v.play(); if (p && p.catch) p.catch(() => {}); }, { once: true });
3748
+ show(8000); hook('program', n, it);
3749
  } else if (!isBad && v.readyState > 1 && Math.abs(v.currentTime - off) > 4) v.currentTime = off; // rudi kwenye 'live'
3750
  last = n; draw(n, it);
3751
  };
 
3756
  v.addEventListener('error', () => { if (last) { bad.add(last.idx); sync(); } });
3757
  v.addEventListener('click', () => show(6000));
3758
  const timer = setInterval(sync, 15000);
3759
+ window.__jtvStop = () => { clearInterval(timer); clearTimeout(hideT); hook('close'); ctx.timers.forEach(clearTimeout); ctx.own.forEach(e => { try { e.remove(); } catch (_) {} }); };
3760
+ hook('open');
3761
  sync();
3762
  }
3763
 
3764
+ async function fetchAllLiveItemsRaw() {
3765
  const items = [];
3766
  let anyOk = false, lastErr = null;
3767
  const posters = await fetchChannelPosters();
3768
 
3769
  try { // JaynesTV (ikiwa ratiba ipo): kadi inaonyesha kipindi kinachochezwa sasa
3770
  const js = await jtvSchedule();
3771
+ if (js) { // kadi ina nembo yako (au picha ya kipindi, kulingana na config.card) na jina la kipindi cha sasa
3772
+ const jc = jtvCfg(js), n = jtvNow(js, Date.now());
3773
+ items.push({ id: 'jaynestv', kind: 'live', title: '● ' + n.item.title, poster: ((jc.card === 'program' ? (n.item.poster || jc.logo) : (jc.logo || n.item.poster)) || null), subtitle: jc.name, imgFallbackOn: true });
3774
+ }
3775
  } catch (_) {}
3776
 
3777
  try {
 
3828
  }
3829
 
3830
  // ── ZILIZOTAFSIRIWA (movies/series za DJ β€” mradi tofauti) ──
3831
+ async function fetchTranslatedListRaw() {
3832
  const card = (m, idPrefix) => ({
3833
  id: idPrefix + m.id, kind: 'translated', title: m.title,
3834
  poster: m.thumb_url || m.poster_url || null, imgFallbackOn: true,
3835
  subtitle: normDj(m.dj) || (m.category === 'series' ? 'Series' : 'Movie'), isSeries: m.category === 'series',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3836
  });
3837
+ const oldP = (async () => { // chanzo cha zamani
3838
+ const r = await fetch(TRANSLATED_BASE + '?limit=100', { headers: { apikey: TRANSLATED_KEY } });
3839
+ if (!r.ok) throw new Error(T('err_generic') + ' ' + r.status);
3840
+ const d = await r.json();
3841
+ if (!d.ok) throw new Error(d.error || 'Seva imekataa ombi.');
3842
+ return (d.movies || []).map(m => card(m, ''));
3843
+ })();
3844
+ const newP = (async () => { // movie-zilizotafsriwa2: public tu (seva inachuja)
3845
+ const r = await fetch(TRANSLATED2_BASE, { headers: { apikey: SUPABASE_ANON_KEY } });
3846
+ if (!r.ok) throw new Error(T('err_generic') + ' ' + r.status);
3847
+ const d = await r.json();
3848
+ return (Array.isArray(d) ? d : []).map(m => card(m, TR2_PREFIX));
3849
+ })();
3850
+ const [a, b] = await Promise.allSettled([oldP, newP]);
3851
+ if (a.status === 'rejected' && b.status === 'rejected') throw a.reason;
3852
+ const fresh = b.status === 'fulfilled' ? b.value : [], older = a.status === 'fulfilled' ? a.value : [];
3853
+ const seen = new Set(fresh.map(x => String(x.title || '').trim().toLowerCase()));
3854
+ return fresh.concat(older.filter(x => !seen.has(String(x.title || '').trim().toLowerCase())));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3855
  }
3856
 
3857
  async function loadTranslated() {
 
3859
  renderSkeletonRails(2);
3860
  try {
3861
  const [items, model] = await Promise.all([fetchTranslatedList(), loadTrModel()]);
3862
+ renderRails(buildTranslatedSections(items, model), false);
 
3863
  } catch (e) {
3864
  railsEl.innerHTML = '';
3865
  railsEl.appendChild(stateBox(t('err_failed', { e: esc(e.message || t('err_generic')) }), true, loadTranslated));
 
4091
  v.addEventListener('ended', () => { const c = window.__pcPlaylist; if (c && typeof c.next === 'function') c.next(); });
4092
  // Tangazo la video kwanza (likishindikana/likiruka, filamu inaanza mara moja)
4093
  v.src = url;
4094
+ try { logTrPlay(currentTranslatedDetail && currentTranslatedDetail.id); } catch (_) {}
4095
  // Route ya player: #/translated/ID/play?se=..&ep=..
4096
  try {
4097
  const id = currentTranslatedDetail && currentTranslatedDetail.id;
 
4192
  // ── MICHEZO (fawa-live) ──
4193
  // Inarudisha orodha bapa (flat) ya mechi zote β€” inatumika na loadSports()
4194
  // (kuonyesha kwa rails, kikundi kwa ligi) na search (kuchuja kwa query).
4195
+ async function fetchAllSportsItemsRaw() {
4196
  const res = await fetch(FAWA_BASE + '/matches', { headers: { Authorization: 'Bearer ' + FAWA_KEY } });
4197
  if (!res.ok) throw new Error(T('err_generic') + ' ' + res.status);
4198
  const d = await res.json();
 
6997
  if (!wrap) return;
6998
  wrap.innerHTML = '<div class="trailer-state">' + T('trailer_loading') + '</div>';
6999
  try {
7000
+ const result = await fetchTrailerJson(subjectId);
 
 
 
 
7001
  if (result.code !== 0) throw new Error(result.message || 'trailer_error');
7002
  // Kagua kila hatua kabla ya kutumia β€” data.trailer, .url zote zinaweza kukosekana
7003
  const trailer = result.data && result.data.trailer;
 
10066
  if (!startRes.ok || !startData.jobId) throw new Error(startData.error || t('dq_err_mux_start'));
10067
  const jobId = startData.jobId;
10068
 
10069
+ const t0 = Date.now();
10070
+ for (let tries = 0; Date.now() - t0 < 900000; tries++) { // hadi dakika 15; muda kati ya maombi unaongezeka (5s -> 15s) kupunguza invocations
10071
+ await new Promise(r => setTimeout(r, Math.min(15000, 5000 + tries * 1000)));
10072
  const st = await fetch(MUX_BASE + '?jobId=' + encodeURIComponent(jobId), {
10073
  headers: { 'Authorization': 'Bearer ' + MUX_KEY, 'apikey': MUX_KEY },
10074
  });
 
10665
  }
10666
  }
10667
  // Config ya Home inatoka Supabase (home-config); ikishindwa tunatumia nakala ya mwisho au chaguo-msingi
10668
+ async function getHomeConfigRaw(){
10669
  try {
10670
  const ctl = new AbortController(); const to = setTimeout(() => ctl.abort(), 3000);
10671
  const r = await fetch(HOME_CFG_URL, { method: 'POST', signal: ctl.signal, headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + SUPABASE_ANON_KEY, apikey: SUPABASE_ANON_KEY }, body: JSON.stringify({ action: 'get' }) });