meky112 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.js ADDED
@@ -0,0 +1,1084 @@
1
+ const API_URL = '/proxy/it/archive'; // Use proxy endpoint
2
+
3
+ // Dynamically determine the proxy server URL.
4
+ // If loaded via localhost:8000, use it. If on Render, use the origin. Otherwise, default to Render proxy URL.
5
+ const PROXY_URL = (window.location.hostname === 'localhost' && window.location.port === '8000')
6
+ ? 'http://localhost:8000'
7
+ : (window.location.hostname.includes('onrender.com')
8
+ ? window.location.origin
9
+ : 'https://tsc-6qr9.onrender.com');
10
+
11
+ if (window.location.protocol === 'file:') {
12
+ const warnMsg = "ATTENZIONE: Stai aprendo l'applicazione direttamente come file locale (file://).\n\n" +
13
+ "Per far funzionare correttamente lo streaming video ed evitare blocchi di sicurezza (CORS/CSP) da parte di Vixcloud,\n" +
14
+ "avvia il server Python con 'python server.py' e apri questo indirizzo nel tuo browser:\n" +
15
+ "http://localhost:8000";
16
+ console.warn(warnMsg);
17
+ alert(warnMsg);
18
+ }
19
+
20
+ let cdnUrl = 'https://cdn.streamingcommunityz.associates'; // Will load dynamically from proxy
21
+ let baseSite = 'https://streamingcommunityz.associates'; // Will load dynamically from proxy
22
+
23
+ // Fetch dynamic config from proxy
24
+ async function loadProxyConfig() {
25
+ try {
26
+ const response = await fetch(`${PROXY_URL}/proxy-config`);
27
+ const config = await response.json();
28
+ if (config) {
29
+ if (config.cdn_site) cdnUrl = config.cdn_site;
30
+ if (config.base_site) baseSite = config.base_site;
31
+ console.log("Loaded dynamic config:", { cdnUrl, baseSite });
32
+ }
33
+ } catch (err) {
34
+ console.warn("Could not load dynamic config from proxy, using fallback:", err);
35
+ }
36
+ }
37
+
38
+ // Global App State
39
+ let currentView = 'gallery'; // 'gallery', 'details', 'player'
40
+ let allTitles = [];
41
+ let fetchedTitles = []; // Cache for current search or archive results
42
+ let focusedGalleryIndex = 0;
43
+
44
+ // Search & Filter Navigation State
45
+ let galleryFocusArea = 'grid'; // 'search' or 'grid'
46
+ let searchFocusedIndex = 0; // 0: input, 1: btn-clear, 2: filter-all, 3: filter-movie, 4: filter-tv
47
+ let activeFilter = 'all'; // 'all', 'movie', 'tv'
48
+
49
+ // Details Navigation State
50
+ let detailsFocusArea = 'buttons'; // 'buttons', 'seasons', or 'episodes'
51
+ let detailsButtonIndex = 0; // 0 for Play, 1 for Close
52
+ let detailsEpisodeIndex = 0;
53
+ let detailsSeasonIndex = 0;
54
+ let activeSeasonNumber = 1;
55
+ let loadedTitleData = null; // Stored metadata of the opened title
56
+
57
+ // DOM Elements
58
+ let galleryEl, detailsEl, playerEl;
59
+ let searchInputEl, btnClearEl, filterAllEl, filterMovieEl, filterTvEl;
60
+
61
+ // Helper to fetch JSON data from the page's data-page attribute
62
+ async function fetchData() {
63
+ const response = await fetch(`${PROXY_URL}${API_URL}`);
64
+ const text = await response.text();
65
+ const parser = new DOMParser();
66
+ const doc = parser.parseFromString(text, "text/html");
67
+ const appDiv = doc.querySelector('[data-page]');
68
+ if (!appDiv) throw new Error('Data page JSON not found');
69
+ const dataAttr = appDiv.getAttribute('data-page');
70
+ const decoded = dataAttr.replace(/"/g, '"').replace(/'/g, "'");
71
+ const json = JSON.parse(decoded);
72
+ return json.props.titles || [];
73
+ }
74
+
75
+ // Fetch title details page
76
+ async function fetchTitleDetails(id, slug) {
77
+ const response = await fetch(`${PROXY_URL}/proxy/it/titles/${id}-${slug}`);
78
+ const text = await response.text();
79
+ const parser = new DOMParser();
80
+ const doc = parser.parseFromString(text, "text/html");
81
+ const appDiv = doc.querySelector('[data-page]');
82
+ if (!appDiv) throw new Error('Detail JSON not found');
83
+ const decoded = appDiv.getAttribute('data-page').replace(/"/g, '"').replace(/'/g, "'");
84
+ return JSON.parse(decoded).props;
85
+ }
86
+
87
+ // Fetch watch page to get embed URL
88
+ async function fetchEmbedUrl(titleId, episodeId = null) {
89
+ let url = `${PROXY_URL}/proxy/it/watch/${titleId}`;
90
+ if (episodeId) {
91
+ url += `?e=${episodeId}`;
92
+ }
93
+ const response = await fetch(url);
94
+ const text = await response.text();
95
+ const parser = new DOMParser();
96
+ const doc = parser.parseFromString(text, "text/html");
97
+ const appDiv = doc.querySelector('[data-page]');
98
+ if (!appDiv) throw new Error('Watch JSON not found');
99
+ const decoded = appDiv.getAttribute('data-page').replace(/"/g, '"').replace(/'/g, "'");
100
+ const props = JSON.parse(decoded).props;
101
+ return props.embedUrl;
102
+ }
103
+
104
+ // Build a gallery item element
105
+ function createItem(item) {
106
+ const div = document.createElement('div');
107
+ div.className = 'card';
108
+
109
+ const img = document.createElement('img');
110
+ const imgObj = item.images.find(i => i.type === 'poster') || item.images.find(i => i.type === 'cover');
111
+ if (imgObj) {
112
+ img.src = imgObj.original_url || `${cdnUrl}/images/${imgObj.filename}`;
113
+ } else {
114
+ img.src = '';
115
+ }
116
+ img.alt = item.name;
117
+
118
+ const info = document.createElement('div');
119
+ info.className = 'info';
120
+ const h3 = document.createElement('h3');
121
+ h3.textContent = item.name;
122
+ info.appendChild(h3);
123
+
124
+ div.appendChild(img);
125
+ div.appendChild(info);
126
+ return div;
127
+ }
128
+
129
+ // Render the gallery with initial archive data
130
+ async function renderGallery() {
131
+ galleryEl = document.getElementById('gallery');
132
+ try {
133
+ fetchedTitles = await fetchData();
134
+ applyFiltersAndRender();
135
+ } catch (err) {
136
+ console.error("Failed to load initial archive:", err);
137
+ }
138
+ }
139
+
140
+ // Perform search via proxy on Enter key
141
+ async function performSearch() {
142
+ const query = searchInputEl.value.trim();
143
+ galleryEl.innerHTML = `<div style="color: #fff; padding: 20px; font-size: 20px; width: 100%;">Ricerca in corso...</div>`;
144
+
145
+ if (query) {
146
+ try {
147
+ const response = await fetch(`${PROXY_URL}/proxy/it/search?q=${encodeURIComponent(query)}`);
148
+ const text = await response.text();
149
+ const parser = new DOMParser();
150
+ const doc = parser.parseFromString(text, "text/html");
151
+ const appDiv = doc.querySelector('[data-page]');
152
+ if (appDiv) {
153
+ const decoded = appDiv.getAttribute('data-page').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
154
+ const props = JSON.parse(decoded).props;
155
+ fetchedTitles = props.titles || [];
156
+ } else {
157
+ fetchedTitles = [];
158
+ }
159
+ } catch (err) {
160
+ console.error("Search error:", err);
161
+ fetchedTitles = [];
162
+ }
163
+ } else {
164
+ try {
165
+ fetchedTitles = await fetchData();
166
+ } catch (err) {
167
+ console.error("Failed to reload archive:", err);
168
+ fetchedTitles = [];
169
+ }
170
+ }
171
+
172
+ applyFiltersAndRender();
173
+ }
174
+
175
+ // Apply current active category filters and render titles inside grid
176
+ function applyFiltersAndRender() {
177
+ let filtered = fetchedTitles;
178
+ if (activeFilter === 'movie') {
179
+ filtered = fetchedTitles.filter(t => t.type === 'movie');
180
+ } else if (activeFilter === 'tv') {
181
+ filtered = fetchedTitles.filter(t => t.type === 'tv');
182
+ }
183
+
184
+ allTitles = filtered;
185
+ focusedGalleryIndex = 0;
186
+
187
+ galleryEl.innerHTML = '';
188
+ if (allTitles.length === 0) {
189
+ galleryEl.innerHTML = `<div style="color: #888; padding: 20px; font-size: 20px; width: 100%;">Nessun titolo trovato per questo filtro.</div>`;
190
+ } else {
191
+ allTitles.forEach(item => galleryEl.appendChild(createItem(item)));
192
+ }
193
+
194
+ updateGalleryFocus();
195
+ }
196
+
197
+ // Select a filter category
198
+ function selectFilter(filterName) {
199
+ activeFilter = filterName;
200
+
201
+ filterAllEl.classList.toggle('active', activeFilter === 'all');
202
+ filterMovieEl.classList.toggle('active', activeFilter === 'movie');
203
+ filterTvEl.classList.toggle('active', activeFilter === 'tv');
204
+
205
+ applyFiltersAndRender();
206
+ }
207
+
208
+ // Update DOM classes to reflect gallery focus
209
+ function updateGalleryFocus() {
210
+ // Remove focus class from all elements in the main gallery/search area
211
+ const focusedElements = document.querySelectorAll('.search-container .focused, #search-bar .focused, .grid .card.focused, .filters .filter-btn.focused, .clear-btn.focused');
212
+ focusedElements.forEach(el => el.classList.remove('focused'));
213
+
214
+ if (galleryFocusArea === 'search') {
215
+ if (searchFocusedIndex === 0) {
216
+ searchInputEl.classList.add('focused');
217
+ } else if (searchFocusedIndex === 1) {
218
+ if (btnClearEl) btnClearEl.classList.add('focused');
219
+ } else {
220
+ const btns = [filterAllEl, filterMovieEl, filterTvEl];
221
+ const targetBtn = btns[searchFocusedIndex - 2];
222
+ if (targetBtn) targetBtn.classList.add('focused');
223
+ }
224
+ } else if (galleryFocusArea === 'grid') {
225
+ const cards = galleryEl.querySelectorAll('.card');
226
+ if (cards.length > 0) {
227
+ if (focusedGalleryIndex >= cards.length) focusedGalleryIndex = cards.length - 1;
228
+ if (focusedGalleryIndex < 0) focusedGalleryIndex = 0;
229
+
230
+ const activeCard = cards[focusedGalleryIndex];
231
+ activeCard.classList.add('focused');
232
+
233
+ activeCard.scrollIntoView({
234
+ behavior: 'smooth',
235
+ block: 'nearest',
236
+ inline: 'nearest'
237
+ });
238
+ }
239
+ }
240
+ }
241
+
242
+ // Helper to determine the number of grid columns dynamically
243
+ function getGridColumns() {
244
+ const cards = document.querySelectorAll('.card');
245
+ if (cards.length <= 1) return 1;
246
+ const firstTop = cards[0].offsetTop;
247
+ for (let i = 1; i < cards.length; i++) {
248
+ if (cards[i].offsetTop > firstTop) {
249
+ return i;
250
+ }
251
+ }
252
+ return cards.length;
253
+ }
254
+
255
+ // Open details view for a title
256
+ function showDetails(item) {
257
+ currentView = 'details';
258
+ detailsEl.style.display = 'flex';
259
+
260
+ // Remove focus class from gallery items
261
+ const activeCard = galleryEl.querySelector('.card.focused');
262
+ if (activeCard) activeCard.classList.remove('focused');
263
+
264
+ detailsEl.innerHTML = `
265
+ <div class="details-top">
266
+ <div class="details-overlay"></div>
267
+ <div class="details-content">
268
+ <h2 class="details-title">Caricamento...</h2>
269
+ </div>
270
+ </div>
271
+ `;
272
+
273
+ // Reset navigation state inside details
274
+ detailsFocusArea = 'buttons';
275
+ detailsButtonIndex = 0;
276
+ detailsEpisodeIndex = 0;
277
+ detailsSeasonIndex = 0;
278
+ activeSeasonNumber = 1;
279
+
280
+ loadDetailsData(item);
281
+ }
282
+
283
+ // Populate details view with fetched metadata
284
+ async function loadDetailsData(item) {
285
+ try {
286
+ const props = await fetchTitleDetails(item.id, item.slug);
287
+ loadedTitleData = props;
288
+
289
+ const titleObj = props.title || item;
290
+ const plotTrans = titleObj.translations.find(t => t.key === 'plot');
291
+
292
+ // Decode HTML entities
293
+ const tempDiv = document.createElement('div');
294
+ tempDiv.innerHTML = plotTrans ? plotTrans.value : 'Trama non disponibile.';
295
+ const plotText = tempDiv.textContent;
296
+
297
+ const releaseYear = titleObj.release_date ? new Date(titleObj.release_date).getFullYear() : (titleObj.release_date_it ? new Date(titleObj.release_date_it).getFullYear() : 'N/D');
298
+
299
+ // Backdrop Image
300
+ const bgImg = titleObj.images.find(i => i.type === 'background') || titleObj.images.find(i => i.type === 'cover');
301
+ const bgUrl = bgImg ? (bgImg.original_url || `${cdnUrl}/images/${bgImg.filename}`) : '';
302
+
303
+ // Logo Image
304
+ const logoImg = titleObj.images.find(i => i.type === 'logo');
305
+ const logoUrl = logoImg ? (logoImg.original_url || `${cdnUrl}/images/${logoImg.filename}`) : '';
306
+
307
+ // Initialize seasons indexes
308
+ if (titleObj.type === 'tv') {
309
+ activeSeasonNumber = props.loadedSeason ? props.loadedSeason.number : 1;
310
+ const seasons = titleObj.seasons || [];
311
+ const sIdx = seasons.findIndex(s => s.number === activeSeasonNumber);
312
+ detailsSeasonIndex = sIdx >= 0 ? sIdx : 0;
313
+ }
314
+
315
+ let buttonsHtml = '';
316
+ if (titleObj.type === 'movie') {
317
+ buttonsHtml = `
318
+ <button class="btn btn-primary btn-play focused">Riproduci</button>
319
+ <button class="btn btn-close">Chiudi</button>
320
+ `;
321
+ } else {
322
+ buttonsHtml = `
323
+ <button class="btn btn-primary btn-play focused">Riproduci S${activeSeasonNumber}:E1</button>
324
+ <button class="btn btn-close">Chiudi</button>
325
+ `;
326
+ }
327
+
328
+ let bottomHtml = '';
329
+ if (titleObj.type === 'tv') {
330
+ const seasons = titleObj.seasons || [];
331
+ let seasonsButtonsHtml = '';
332
+ seasons.forEach((s, idx) => {
333
+ const isCurrent = s.number === activeSeasonNumber;
334
+ seasonsButtonsHtml += `
335
+ <button class="season-btn ${isCurrent ? 'active' : ''}" data-number="${s.number}" data-index="${idx}">
336
+ Stagione ${s.number}
337
+ </button>
338
+ `;
339
+ });
340
+
341
+ const episodes = props.loadedSeason ? (props.loadedSeason.episodes || []) : [];
342
+ let episodesListHtml = '';
343
+ episodes.forEach((ep, idx) => {
344
+ episodesListHtml += `
345
+ <div class="episode-card" data-index="${idx}" data-id="${ep.id}">
346
+ <div class="episode-number">Ep. ${ep.number}</div>
347
+ <div class="episode-name">${ep.name || 'Senza nome'}</div>
348
+ </div>
349
+ `;
350
+ });
351
+
352
+ bottomHtml = `
353
+ <div class="details-bottom">
354
+ <div class="seasons-list-row">
355
+ ${seasonsButtonsHtml}
356
+ </div>
357
+ <div class="episodes-list">
358
+ ${episodesListHtml || '<div style="color:#aaa; padding:10px;">Nessun episodio caricato</div>'}
359
+ </div>
360
+ </div>
361
+ `;
362
+ }
363
+
364
+ detailsEl.innerHTML = `
365
+ <div class="details-top">
366
+ ${bgUrl ? `<img class="details-backdrop" src="${bgUrl}" alt="backdrop" />` : ''}
367
+ <div class="details-overlay"></div>
368
+ <div class="details-content">
369
+ ${logoUrl ? `<img class="details-logo" src="${logoUrl}" alt="logo" />` : `<h2 class="details-title">${titleObj.name}</h2>`}
370
+ <div class="details-meta">
371
+ <span class="rating">★ ${titleObj.score || '0.0'}</span>
372
+ <span class="year">${releaseYear}</span>
373
+ <span class="badge">${titleObj.quality || 'HD'}</span>
374
+ ${titleObj.type === 'tv' ? `<span class="badge">${titleObj.seasons_count} Stagioni</span>` : ''}
375
+ </div>
376
+ <p class="details-plot">${plotText}</p>
377
+ <div class="details-buttons">
378
+ ${buttonsHtml}
379
+ </div>
380
+ </div>
381
+ </div>
382
+ ${bottomHtml}
383
+ `;
384
+
385
+ updateDetailsFocus();
386
+
387
+ } catch (err) {
388
+ console.error("Failed to load details:", err);
389
+ detailsEl.innerHTML = `
390
+ <div class="details-top">
391
+ <div class="details-overlay"></div>
392
+ <div class="details-content">
393
+ <h2 class="details-title">Errore</h2>
394
+ <p>Impossibile caricare i dettagli di questo titolo.</p>
395
+ <div class="details-buttons">
396
+ <button class="btn btn-close focused">Chiudi</button>
397
+ </div>
398
+ </div>
399
+ </div>
400
+ `;
401
+ updateDetailsFocus();
402
+ }
403
+ }
404
+
405
+ // Fetch episodes of a specific season dynamically and rerender the bottom UI
406
+ async function changeSeason(seasonNumber) {
407
+ if (!loadedTitleData || !loadedTitleData.title) return;
408
+
409
+ const titleObj = loadedTitleData.title;
410
+ activeSeasonNumber = seasonNumber;
411
+
412
+ // Show loading indicator in episodes list
413
+ const episodesListEl = detailsEl.querySelector('.episodes-list');
414
+ if (episodesListEl) {
415
+ episodesListEl.innerHTML = `<div style="color: #fff; padding: 10px; font-size: 14px;">Caricamento episodi...</div>`;
416
+ }
417
+
418
+ // Update Play button label text to reflect selected season
419
+ const playBtn = detailsEl.querySelector('.details-buttons .btn-play');
420
+ if (playBtn) {
421
+ playBtn.textContent = `Riproduci S${activeSeasonNumber}:E1`;
422
+ }
423
+
424
+ try {
425
+ const response = await fetch(`${PROXY_URL}/proxy/it/titles/${titleObj.id}-${titleObj.slug}/season-${seasonNumber}`);
426
+ const text = await response.text();
427
+ const parser = new DOMParser();
428
+ const doc = parser.parseFromString(text, "text/html");
429
+ const appDiv = doc.querySelector('[data-page]');
430
+ if (!appDiv) throw new Error('Data page JSON not found');
431
+ const decoded = appDiv.getAttribute('data-page').replace(/&quot;/g, '"').replace(/&#39;/g, "'");
432
+ const newProps = JSON.parse(decoded).props;
433
+
434
+ // Update loadedTitleData
435
+ loadedTitleData = newProps;
436
+
437
+ // Rerender the bottom section!
438
+ const bottomContainer = detailsEl.querySelector('.details-bottom');
439
+ if (bottomContainer) {
440
+ const seasons = titleObj.seasons || [];
441
+ let seasonsButtonsHtml = '';
442
+ seasons.forEach((s, idx) => {
443
+ const isCurrent = s.number === activeSeasonNumber;
444
+ seasonsButtonsHtml += `
445
+ <button class="season-btn ${isCurrent ? 'active' : ''}" data-number="${s.number}" data-index="${idx}">
446
+ Stagione ${s.number}
447
+ </button>
448
+ `;
449
+ });
450
+
451
+ const episodes = newProps.loadedSeason ? (newProps.loadedSeason.episodes || []) : [];
452
+ let episodesListHtml = '';
453
+ episodes.forEach((ep, idx) => {
454
+ episodesListHtml += `
455
+ <div class="episode-card" data-index="${idx}" data-id="${ep.id}">
456
+ <div class="episode-number">Ep. ${ep.number}</div>
457
+ <div class="episode-name">${ep.name || 'Senza nome'}</div>
458
+ </div>
459
+ `;
460
+ });
461
+
462
+ bottomContainer.innerHTML = `
463
+ <div class="seasons-list-row">
464
+ ${seasonsButtonsHtml}
465
+ </div>
466
+ <div class="episodes-list">
467
+ ${episodesListHtml || '<div style="color:#aaa; padding:10px;">Nessun episodio caricato</div>'}
468
+ </div>
469
+ `;
470
+ }
471
+
472
+ updateDetailsFocus();
473
+
474
+ } catch (err) {
475
+ console.error("Failed to load season:", err);
476
+ if (episodesListEl) {
477
+ episodesListEl.innerHTML = `<div style="color: #ff5555; padding: 10px; font-size: 14px;">Errore nel caricamento degli episodi.</div>`;
478
+ }
479
+ }
480
+ }
481
+
482
+ // Update DOM classes to reflect the currently focused element in details view
483
+ function updateDetailsFocus() {
484
+ const focusedElements = detailsEl.querySelectorAll('.focused');
485
+ focusedElements.forEach(el => el.classList.remove('focused'));
486
+
487
+ if (detailsFocusArea === 'buttons') {
488
+ const buttons = detailsEl.querySelectorAll('.details-buttons .btn');
489
+ if (buttons.length > 0) {
490
+ if (detailsButtonIndex >= buttons.length) detailsButtonIndex = buttons.length - 1;
491
+ if (detailsButtonIndex < 0) detailsButtonIndex = 0;
492
+ buttons[detailsButtonIndex].classList.add('focused');
493
+ }
494
+ } else if (detailsFocusArea === 'seasons') {
495
+ const seasonBtns = detailsEl.querySelectorAll('.seasons-list-row .season-btn');
496
+ if (seasonBtns.length > 0) {
497
+ if (detailsSeasonIndex >= seasonBtns.length) detailsSeasonIndex = seasonBtns.length - 1;
498
+ if (detailsSeasonIndex < 0) detailsSeasonIndex = 0;
499
+
500
+ const activeBtn = seasonBtns[detailsSeasonIndex];
501
+ activeBtn.classList.add('focused');
502
+
503
+ activeBtn.scrollIntoView({
504
+ behavior: 'smooth',
505
+ block: 'nearest',
506
+ inline: 'nearest'
507
+ });
508
+ }
509
+ } else if (detailsFocusArea === 'episodes') {
510
+ const episodes = detailsEl.querySelectorAll('.episodes-list .episode-card');
511
+ if (episodes.length > 0) {
512
+ if (detailsEpisodeIndex >= episodes.length) detailsEpisodeIndex = episodes.length - 1;
513
+ if (detailsEpisodeIndex < 0) detailsEpisodeIndex = 0;
514
+
515
+ const activeCard = episodes[detailsEpisodeIndex];
516
+ activeCard.classList.add('focused');
517
+
518
+ activeCard.scrollIntoView({
519
+ behavior: 'smooth',
520
+ block: 'nearest',
521
+ inline: 'nearest'
522
+ });
523
+ }
524
+ }
525
+ }// Get the video element from the player view
526
+ function getPlayerVideo() {
527
+ return document.getElementById('native-player');
528
+ }
529
+
530
+ // Extract the direct HLS stream URL from the StreamingCommunity embed URL
531
+ async function extractStreamUrl(embedUrl) {
532
+ // 1. Fetch StreamingCommunity embed page (proxied)
533
+ let scEmbedUrl = embedUrl;
534
+ if (scEmbedUrl.includes(baseSite)) {
535
+ scEmbedUrl = scEmbedUrl.replace(baseSite, PROXY_URL + '/proxy');
536
+ }
537
+
538
+ console.log("Fetching SC embed iframe:", scEmbedUrl);
539
+ const scResponse = await fetch(scEmbedUrl);
540
+ const scHtml = await scResponse.text();
541
+
542
+ // Extract Vixcloud iframe URL
543
+ const iframeMatch = scHtml.match(/<iframe[^>]+src=["']([^"']+)["']/);
544
+ if (!iframeMatch) throw new Error("Vixcloud iframe non trovato nella pagina di embed");
545
+ let vixcloudUrl = iframeMatch[1].replace(/&amp;/g, '&');
546
+
547
+ // 2. Fetch Vixcloud page (proxied)
548
+ if (vixcloudUrl.includes('https://vixcloud.co')) {
549
+ vixcloudUrl = vixcloudUrl.replace('https://vixcloud.co', PROXY_URL + '/vixcloud');
550
+ }
551
+
552
+ console.log("Fetching Vixcloud HTML:", vixcloudUrl);
553
+ const vixResponse = await fetch(vixcloudUrl);
554
+ const vixHtml = await vixResponse.text();
555
+
556
+ // Extract window.masterPlaylist params using regex
557
+ const tokenMatch = vixHtml.match(/['"]token['"]\s*:\s*['"]([^'"]+)['"]/);
558
+ const expiresMatch = vixHtml.match(/['"]expires['"]\s*:\s*['"]([^'"]+)['"]/);
559
+ const urlMatch = vixHtml.match(/url\s*:\s*['"](https?:\/\/[^'"]+\/playlist\/[^'"]+)['"]/);
560
+
561
+ if (!tokenMatch || !expiresMatch || !urlMatch) {
562
+ throw new Error("Impossibile estrarre i parametri di streaming da Vixcloud");
563
+ }
564
+
565
+ const token = tokenMatch[1];
566
+ const expires = expiresMatch[1];
567
+ const playlistBaseUrl = urlMatch[1];
568
+
569
+ // 3. Construct stream URL (clean query params from playlistBaseUrl first to prevent double question marks)
570
+ const cleanPlaylistUrl = playlistBaseUrl.split('?')[0];
571
+ let streamUrl = `${cleanPlaylistUrl}?token=${token}&expires=${expires}&b=1`;
572
+ console.log("Constructed playlist URL:", streamUrl);
573
+
574
+ // Rewrite to proxy only on local PC development (localhost) to bypass CORS.
575
+ // On Tizen/TV, we stream directly from vixcloud.co to avoid the 403 Cloudflare blocks on cloud proxies (like Render).
576
+ const isLocalhost = (window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1');
577
+ const isTizen = (window.tizen !== undefined || navigator.userAgent.includes('Tizen'));
578
+ if (isLocalhost && !isTizen && streamUrl.includes('https://vixcloud.co')) {
579
+ streamUrl = streamUrl.replace('https://vixcloud.co', PROXY_URL + '/vixcloud');
580
+ }
581
+
582
+ return streamUrl;
583
+ }
584
+
585
+ // Launch Video Player using HTML5 video tag with direct HLS stream
586
+ async function playTitle(titleId, episodeId = null) {
587
+ try {
588
+ currentView = 'player';
589
+ playerEl.style.display = 'block';
590
+ playerEl.innerHTML = `
591
+ <div id="debug-overlay" style="position: absolute; top: 10px; left: 10px; z-index: 9999; color: #0f0; background: rgba(0,0,0,0.8); padding: 10px; font-size: 14px; font-family: monospace; white-space: pre-wrap; max-width: 80%; pointer-events: none;">[Debug Log]</div>
592
+ <video id="native-player" autoplay style="width: 100%; height: 100%; background: #000;"></video>
593
+ `;
594
+
595
+ const debugLog = document.getElementById('debug-overlay');
596
+ const log = (msg) => { debugLog.innerHTML += `\n${msg}`; console.log(msg); };
597
+
598
+ log(`Fetching embed URL...`);
599
+ const embedUrl = await fetchEmbedUrl(titleId, episodeId);
600
+
601
+ log(`Extracting stream URL from: ${embedUrl.substring(0, 50)}...`);
602
+ const streamUrl = await extractStreamUrl(embedUrl);
603
+ log(`Stream URL ready.`);
604
+
605
+ const video = document.getElementById('native-player');
606
+
607
+ video.addEventListener('error', (e) => {
608
+ const err = video.error;
609
+ log(`Native Video Error: ${err ? err.code + ' ' + err.message : 'Unknown'}`);
610
+ });
611
+
612
+ const isSafariPlayer = navigator.userAgent.includes('Safari') && !navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Tizen');
613
+ const isTizenPlayer = navigator.userAgent.includes('Tizen') || window.tizen !== undefined;
614
+ const hasAVPlay = (typeof webapis !== 'undefined' && webapis.avplay);
615
+
616
+ log(`Browser detection: Safari=${isSafariPlayer}, Tizen=${isTizenPlayer}, AVPlay=${hasAVPlay}`);
617
+
618
+ if (hasAVPlay) {
619
+ log(`Using Tizen AVPlay API...`);
620
+ playerEl.innerHTML += `<object id="av-player" type="application/avplayer" style="width: 100%; height: 100%; position: absolute; top:0; left:0; z-index: 10;"></object>`;
621
+ document.getElementById('debug-overlay').style.zIndex = "9999";
622
+
623
+ try {
624
+ webapis.avplay.open(streamUrl);
625
+ webapis.avplay.setDisplayRect(0, 0, window.innerWidth, window.innerHeight);
626
+
627
+ webapis.avplay.setListener({
628
+ onbufferingstart: function() { log("AVPlay: Buffering start"); },
629
+ onbufferingprogress: function(percent) { /* log("AVPlay: Buffering " + percent + "%"); */ },
630
+ onbufferingcomplete: function() { log("AVPlay: Buffering complete"); },
631
+ onerror: function(eventType) { log("AVPlay Error: " + eventType); },
632
+ onevent: function(eventType, eventData) { log("AVPlay Event: " + eventType + " " + (eventData || "")); },
633
+ onstreamcompleted: function() { log("AVPlay: Stream completed"); stopPlayer(); }
634
+ });
635
+
636
+ try {
637
+ const props = { "UserAgent": navigator.userAgent };
638
+ webapis.avplay.setStreamingProperty("SET_PROPERTIES", JSON.stringify(props));
639
+ } catch(e) { log("AVPlay setProperty warning: " + e.message); }
640
+
641
+ webapis.avplay.prepareAsync(function() {
642
+ log("AVPlay: prepareAsync success, starting playback...");
643
+ webapis.avplay.play();
644
+ }, function(error) {
645
+ log("AVPlay prepareAsync error: " + error.name + " " + error.message);
646
+ });
647
+
648
+ playerEl._hasAVPlay = true;
649
+ } catch (e) {
650
+ log(`AVPlay Exception: ${e.name} ${e.message}`);
651
+ }
652
+ } else if ((isSafariPlayer || isTizenPlayer) && video.canPlayType('application/vnd.apple.mpegurl')) {
653
+ log(`Using Native Player (Safari/Tizen)...`);
654
+ video.src = streamUrl;
655
+ video.play().catch(e => log(`Play failed: ${e.name} - ${e.message}`));
656
+ } else if (typeof Hls !== 'undefined' && Hls.isSupported()) {
657
+ log(`Using hls.js player (Version: ${Hls.version || 'unknown'})...`);
658
+ const hls = new Hls({
659
+ maxMaxBufferLength: 10,
660
+ enableWorker: true,
661
+ debug: false
662
+ });
663
+ hls.loadSource(streamUrl);
664
+ hls.attachMedia(video);
665
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
666
+ log(`HLS Manifest parsed. Attempting play...`);
667
+ video.play().catch(e => log(`Play failed: ${e.message}`));
668
+ });
669
+ hls.on(Hls.Events.ERROR, function(event, data) {
670
+ log(`HLS Error [${data.type}]: ${data.details}`);
671
+ if (data.fatal) {
672
+ log(`Fatal error! Trying to recover...`);
673
+ switch (data.type) {
674
+ case Hls.ErrorTypes.NETWORK_ERROR:
675
+ hls.startLoad();
676
+ break;
677
+ case Hls.ErrorTypes.MEDIA_ERROR:
678
+ hls.recoverMediaError();
679
+ break;
680
+ default:
681
+ hls.destroy();
682
+ log(`HLS Destroyed.`);
683
+ break;
684
+ }
685
+ }
686
+ });
687
+ playerEl._hlsInstance = hls;
688
+ } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
689
+ log(`Fallback Native Player (Hls.js not supported)...`);
690
+ video.src = streamUrl;
691
+ video.play().catch(e => log(`Play failed: ${e.message}`));
692
+ } else {
693
+ log(`Error: Browser does not support HLS or Hls.js`);
694
+ throw new Error("Il tuo browser non supporta la riproduzione HLS.");
695
+ }
696
+ } catch (err) {
697
+ console.error(err);
698
+ if (document.getElementById('debug-overlay')) {
699
+ document.getElementById('debug-overlay').innerHTML += `\nEXCEPTION: ${err.message}`;
700
+ } else {
701
+ alert("Errore caricamento player: " + err.message);
702
+ }
703
+ setTimeout(() => stopPlayer(), 10000); // Wait 10s to read log before closing
704
+ }
705
+ }
706
+
707
+ // Close player and return to details
708
+ function stopPlayer() {
709
+ currentView = 'details';
710
+ playerEl.style.display = 'none';
711
+
712
+ if (playerEl._hasAVPlay && typeof webapis !== 'undefined' && webapis.avplay) {
713
+ try {
714
+ webapis.avplay.stop();
715
+ webapis.avplay.close();
716
+ } catch (e) {}
717
+ playerEl._hasAVPlay = false;
718
+ }
719
+
720
+ // Clean up hls.js instance if exists
721
+ if (playerEl._hlsInstance) {
722
+ try {
723
+ playerEl._hlsInstance.destroy();
724
+ } catch (e) {}
725
+ playerEl._hlsInstance = null;
726
+ }
727
+
728
+ playerEl.innerHTML = ''; // Destroys video element to stop sound and video
729
+ updateDetailsFocus();
730
+ }
731
+
732
+ // Keyboard and Remote Control key down mapping
733
+ function isBackKey(key, keyCode) {
734
+ return key === 'Backspace' || key === 'Escape' || key === 'ArrowBack' || key === 'Back' || key === 'BrowserBack' || key === '\\' || key === '/' || keyCode === 10009 || key === 'XF86Backspace';
735
+ }
736
+
737
+ function handleGalleryKeys(key, keyCode, e) {
738
+ if (galleryFocusArea === 'search') {
739
+ if (key === 'ArrowRight') {
740
+ searchFocusedIndex = (searchFocusedIndex + 1) % 5;
741
+ updateGalleryFocus();
742
+ } else if (key === 'ArrowLeft') {
743
+ searchFocusedIndex = (searchFocusedIndex - 1 + 5) % 5;
744
+ updateGalleryFocus();
745
+ } else if (key === 'ArrowDown') {
746
+ const cards = galleryEl.querySelectorAll('.card');
747
+ if (cards.length > 0) {
748
+ galleryFocusArea = 'grid';
749
+ focusedGalleryIndex = 0;
750
+ updateGalleryFocus();
751
+ }
752
+ } else if (key === 'Enter') {
753
+ if (searchFocusedIndex === 0) {
754
+ if (e) {
755
+ e.preventDefault();
756
+ e.stopPropagation();
757
+ }
758
+ searchInputEl.focus();
759
+ } else if (searchFocusedIndex === 1) {
760
+ if (searchInputEl) searchInputEl.value = '';
761
+ performSearch();
762
+ } else if (searchFocusedIndex === 2) {
763
+ selectFilter('all');
764
+ } else if (searchFocusedIndex === 3) {
765
+ selectFilter('movie');
766
+ } else if (searchFocusedIndex === 4) {
767
+ selectFilter('tv');
768
+ }
769
+ }
770
+ return;
771
+ }
772
+
773
+ const cards = galleryEl.querySelectorAll('.card');
774
+ if (!cards.length) return;
775
+
776
+ const cols = getGridColumns();
777
+ let nextIndex = focusedGalleryIndex;
778
+
779
+ if (key === 'ArrowRight') {
780
+ nextIndex = (focusedGalleryIndex + 1) % cards.length;
781
+ } else if (key === 'ArrowLeft') {
782
+ nextIndex = (focusedGalleryIndex - 1 + cards.length) % cards.length;
783
+ } else if (key === 'ArrowDown') {
784
+ if (focusedGalleryIndex + cols < cards.length) {
785
+ nextIndex = focusedGalleryIndex + cols;
786
+ }
787
+ } else if (key === 'ArrowUp') {
788
+ if (focusedGalleryIndex - cols >= 0) {
789
+ nextIndex = focusedGalleryIndex - cols;
790
+ } else {
791
+ galleryFocusArea = 'search';
792
+ searchFocusedIndex = 0; // Default to input
793
+ updateGalleryFocus();
794
+ return;
795
+ }
796
+ } else if (key === 'Enter') {
797
+ const activeItem = allTitles[focusedGalleryIndex];
798
+ if (activeItem) {
799
+ showDetails(activeItem);
800
+ }
801
+ return;
802
+ }
803
+
804
+ if (nextIndex !== focusedGalleryIndex) {
805
+ cards[focusedGalleryIndex].classList.remove('focused');
806
+ focusedGalleryIndex = nextIndex;
807
+ cards[focusedGalleryIndex].classList.add('focused');
808
+ cards[focusedGalleryIndex].scrollIntoView({
809
+ behavior: 'smooth',
810
+ block: 'nearest'
811
+ });
812
+ }
813
+ }
814
+
815
+ function handleDetailsKeys(key, keyCode) {
816
+ if (isBackKey(key, keyCode)) {
817
+ currentView = 'gallery';
818
+ detailsEl.style.display = 'none';
819
+ loadedTitleData = null;
820
+ updateGalleryFocus();
821
+ return;
822
+ }
823
+
824
+ if (detailsFocusArea === 'buttons') {
825
+ if (key === 'ArrowRight') {
826
+ detailsButtonIndex = 1; // Chiudi button
827
+ updateDetailsFocus();
828
+ } else if (key === 'ArrowLeft') {
829
+ detailsButtonIndex = 0; // Play button
830
+ updateDetailsFocus();
831
+ } else if (key === 'ArrowDown') {
832
+ const titleObj = loadedTitleData ? loadedTitleData.title : null;
833
+ if (titleObj && titleObj.type === 'tv') {
834
+ const seasonBtns = detailsEl.querySelectorAll('.seasons-list-row .season-btn');
835
+ if (seasonBtns.length > 0) {
836
+ detailsFocusArea = 'seasons';
837
+ updateDetailsFocus();
838
+ }
839
+ }
840
+ } else if (key === 'Enter') {
841
+ if (detailsButtonIndex === 0) {
842
+ const titleObj = loadedTitleData.title;
843
+ if (titleObj.type === 'movie') {
844
+ playTitle(titleObj.id);
845
+ } else {
846
+ const episodes = (loadedTitleData.loadedSeason && loadedTitleData.loadedSeason.episodes) || [];
847
+ if (episodes.length > 0) {
848
+ playTitle(titleObj.id, episodes[0].id);
849
+ } else {
850
+ alert("Nessun episodio disponibile.");
851
+ }
852
+ }
853
+ } else {
854
+ // Close details view
855
+ handleDetailsKeys('Escape');
856
+ }
857
+ }
858
+ } else if (detailsFocusArea === 'seasons') {
859
+ const seasonBtns = detailsEl.querySelectorAll('.seasons-list-row .season-btn');
860
+ if (key === 'ArrowRight') {
861
+ if (detailsSeasonIndex + 1 < seasonBtns.length) {
862
+ detailsSeasonIndex++;
863
+ updateDetailsFocus();
864
+ }
865
+ } else if (key === 'ArrowLeft') {
866
+ if (detailsSeasonIndex - 1 >= 0) {
867
+ detailsSeasonIndex--;
868
+ updateDetailsFocus();
869
+ }
870
+ } else if (key === 'ArrowUp') {
871
+ detailsFocusArea = 'buttons';
872
+ updateDetailsFocus();
873
+ } else if (key === 'ArrowDown') {
874
+ const episodes = detailsEl.querySelectorAll('.episodes-list .episode-card');
875
+ if (episodes.length > 0) {
876
+ detailsFocusArea = 'episodes';
877
+ detailsEpisodeIndex = 0;
878
+ updateDetailsFocus();
879
+ }
880
+ } else if (key === 'Enter') {
881
+ const activeBtn = seasonBtns[detailsSeasonIndex];
882
+ if (activeBtn) {
883
+ const sNum = parseInt(activeBtn.getAttribute('data-number'));
884
+ if (sNum && sNum !== activeSeasonNumber) {
885
+ changeSeason(sNum);
886
+ }
887
+ }
888
+ }
889
+ } else if (detailsFocusArea === 'episodes') {
890
+ const episodes = detailsEl.querySelectorAll('.episodes-list .episode-card');
891
+ if (key === 'ArrowRight') {
892
+ if (detailsEpisodeIndex + 1 < episodes.length) {
893
+ detailsEpisodeIndex++;
894
+ updateDetailsFocus();
895
+ }
896
+ } else if (key === 'ArrowLeft') {
897
+ if (detailsEpisodeIndex - 1 >= 0) {
898
+ detailsEpisodeIndex--;
899
+ updateDetailsFocus();
900
+ }
901
+ } else if (key === 'ArrowUp') {
902
+ const titleObj = loadedTitleData ? loadedTitleData.title : null;
903
+ if (titleObj && titleObj.type === 'tv') {
904
+ detailsFocusArea = 'seasons';
905
+ } else {
906
+ detailsFocusArea = 'buttons';
907
+ }
908
+ updateDetailsFocus();
909
+ } else if (key === 'Enter') {
910
+ const titleObj = loadedTitleData.title;
911
+ const episodesList = (loadedTitleData.loadedSeason && loadedTitleData.loadedSeason.episodes) || [];
912
+ const activeEp = episodesList[detailsEpisodeIndex];
913
+ if (activeEp) {
914
+ playTitle(titleObj.id, activeEp.id);
915
+ }
916
+ }
917
+ }
918
+ }
919
+
920
+ function handlePlayerKeys(key, keyCode) {
921
+ if (isBackKey(key, keyCode)) {
922
+ stopPlayer();
923
+ return;
924
+ }
925
+
926
+ const video = getPlayerVideo();
927
+ if (!video) return;
928
+
929
+ // Play/Pause controls (Toggle / Play / Pause keys)
930
+ const isPlayKey = key === 'MediaPlay' || keyCode === 415;
931
+ const isPauseKey = key === 'MediaPause' || keyCode === 19;
932
+ const isToggleKey = key === 'Enter' || key === ' ' || key === 'MediaPlayPause' || keyCode === 10252;
933
+
934
+ if (isPlayKey) {
935
+ try { video.play(); } catch (err) {}
936
+ } else if (isPauseKey) {
937
+ try { video.pause(); } catch (err) {}
938
+ } else if (isToggleKey) {
939
+ try {
940
+ if (video.paused) {
941
+ video.play();
942
+ } else {
943
+ video.pause();
944
+ }
945
+ } catch (err) {}
946
+ }
947
+
948
+ // Seek controls with Left/Right Arrows
949
+ if (key === 'ArrowRight') {
950
+ try {
951
+ video.currentTime += 10; // Seek forward 10s
952
+ } catch (err) {}
953
+ } else if (key === 'ArrowLeft') {
954
+ try {
955
+ video.currentTime -= 10; // Seek backward 10s
956
+ } catch (err) {}
957
+ }
958
+ }
959
+
960
+ // Bind overall remote control events
961
+ function bindRemote() {
962
+ updateGalleryFocus();
963
+
964
+ // Exit application helper
965
+ function exitApp() {
966
+ console.log("Exiting application...");
967
+ if (window.tizen && tizen.application) {
968
+ try {
969
+ tizen.application.getCurrentApplication().exit();
970
+ return;
971
+ } catch (err) {
972
+ console.warn("Tizen application exit failed:", err);
973
+ }
974
+ }
975
+ try {
976
+ window.close();
977
+ } catch (e) {}
978
+ try {
979
+ if (window.history && window.history.length > 1) {
980
+ window.history.back();
981
+ }
982
+ } catch (e) {}
983
+ try {
984
+ window.location.href = "about:blank";
985
+ } catch (e) {}
986
+ }
987
+
988
+ // Force exit when app goes to background (pressed Home button), reload on resume to start fresh
989
+ document.addEventListener('visibilitychange', () => {
990
+ if (document.hidden) {
991
+ if (currentView === 'player') stopPlayer();
992
+ exitApp();
993
+ } else {
994
+ window.location.reload();
995
+ }
996
+ });
997
+ document.addEventListener('webkitvisibilitychange', () => {
998
+ if (document.webkitHidden) {
999
+ if (currentView === 'player') stopPlayer();
1000
+ exitApp();
1001
+ } else {
1002
+ window.location.reload();
1003
+ }
1004
+ });
1005
+
1006
+ document.addEventListener('keydown', e => {
1007
+ // Intercept Back key to prevent default TV actions (like exiting app) if we are in details/player views
1008
+ if (currentView !== 'gallery' && isBackKey(e.key, e.keyCode)) {
1009
+ e.preventDefault();
1010
+ }
1011
+
1012
+ if (currentView === 'gallery') {
1013
+ // If the input has focus, let the browser capture key events (except Escape/Backspace which exits focus)
1014
+ if (document.activeElement === searchInputEl) {
1015
+ if (isBackKey(e.key, e.keyCode)) {
1016
+ searchInputEl.blur();
1017
+ updateGalleryFocus();
1018
+ e.preventDefault();
1019
+ }
1020
+ return; // Don't intercept normal typing keys
1021
+ }
1022
+
1023
+ // If not in input, and back key is pressed, exit application
1024
+ if (isBackKey(e.key, e.keyCode)) {
1025
+ e.preventDefault();
1026
+ exitApp();
1027
+ return;
1028
+ }
1029
+
1030
+ handleGalleryKeys(e.key, e.keyCode, e);
1031
+ } else if (currentView === 'details') {
1032
+ handleDetailsKeys(e.key, e.keyCode);
1033
+ } else if (currentView === 'player') {
1034
+ handlePlayerKeys(e.key, e.keyCode);
1035
+ }
1036
+ });
1037
+ }
1038
+
1039
+ window.addEventListener('load', async () => {
1040
+ try {
1041
+ detailsEl = document.getElementById('details-view');
1042
+ playerEl = document.getElementById('player-view');
1043
+
1044
+ // Bind search and filter DOM elements
1045
+ searchInputEl = document.getElementById('search-input');
1046
+ btnClearEl = document.getElementById('btn-clear');
1047
+ filterAllEl = document.getElementById('filter-all');
1048
+ filterMovieEl = document.getElementById('filter-movie');
1049
+ filterTvEl = document.getElementById('filter-tv');
1050
+
1051
+ if (btnClearEl) {
1052
+ btnClearEl.addEventListener('click', () => {
1053
+ if (searchInputEl) searchInputEl.value = '';
1054
+ performSearch();
1055
+ });
1056
+ }
1057
+
1058
+ // Bind Enter key listener inside search input
1059
+ searchInputEl.addEventListener('keydown', e => {
1060
+ if (e.key === 'Enter') {
1061
+ performSearch();
1062
+ searchInputEl.blur();
1063
+ e.preventDefault();
1064
+ e.stopPropagation();
1065
+ }
1066
+ });
1067
+
1068
+ // Bind Form submit (catches virtual keyboard "Done" / "Fatto" / Enter arrow button clicks)
1069
+ const searchForm = document.getElementById('search-form');
1070
+ if (searchForm) {
1071
+ searchForm.addEventListener('submit', e => {
1072
+ e.preventDefault();
1073
+ performSearch();
1074
+ searchInputEl.blur();
1075
+ });
1076
+ }
1077
+
1078
+ await loadProxyConfig();
1079
+ await renderGallery();
1080
+ bindRemote();
1081
+ } catch (err) {
1082
+ console.error(err);
1083
+ }
1084
+ });