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/.github/workflows/publish.yml +22 -0
- package/app.js +1084 -0
- package/config.json +3 -0
- package/index.html +28 -0
- package/package.json +9 -0
- package/requirements.txt +1 -0
- package/server.py +278 -0
- package/style.css +452 -0
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(/"/g, '"').replace(/'/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(/"/g, '"').replace(/'/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(/&/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
|
+
});
|