titanito commited on
Commit
636a47f
·
verified ·
1 Parent(s): 911d2bf

Add 2 files

Browse files
Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +1084 -19
README.md CHANGED
@@ -1,10 +1,12 @@
1
  ---
2
- title: Iptv Player Premium
3
- emoji: 👀
4
- colorFrom: pink
5
- colorTo: green
6
  sdk: static
7
  pinned: false
 
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
1
  ---
2
+ title: iptv-player-premium
3
+ emoji: 🐳
4
+ colorFrom: gray
5
+ colorTo: red
6
  sdk: static
7
  pinned: false
8
+ tags:
9
+ - deepsite
10
  ---
11
 
12
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
index.html CHANGED
@@ -1,19 +1,1084 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>IPTV Player Premium</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
9
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
10
+ <style>
11
+ /* Estilos CSS personalizados */
12
+ .channel-item {
13
+ transition: transform 0.2s, box-shadow 0.2s;
14
+ }
15
+ .channel-item:hover {
16
+ transform: translateY(-5px);
17
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
18
+ }
19
+ #videoPlayer {
20
+ background-color: #000;
21
+ }
22
+ .loading-spinner {
23
+ display: inline-block;
24
+ width: 20px;
25
+ height: 20px;
26
+ border: 3px solid rgba(255,255,255,.3);
27
+ border-radius: 50%;
28
+ border-top-color: #fff;
29
+ animation: spin 1s ease-in-out infinite;
30
+ }
31
+ @keyframes spin {
32
+ to { transform: rotate(360deg); }
33
+ }
34
+ .source-tab {
35
+ transition: all 0.3s ease;
36
+ }
37
+ .source-tab.active {
38
+ background-color: #3b82f6;
39
+ color: white;
40
+ }
41
+ .toast {
42
+ position: fixed;
43
+ top: 20px;
44
+ right: 20px;
45
+ padding: 15px 25px;
46
+ background: #4f46e5;
47
+ color: white;
48
+ border-radius: 8px;
49
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
50
+ transform: translateX(200%);
51
+ transition: transform 0.3s ease-out;
52
+ z-index: 1000;
53
+ display: flex;
54
+ align-items: center;
55
+ }
56
+ .toast.show {
57
+ transform: translateX(0);
58
+ }
59
+ .toast.error {
60
+ background: #ef4444;
61
+ }
62
+ .toast.warning {
63
+ background: #f59e0b;
64
+ }
65
+ .toast.success {
66
+ background: #10b981;
67
+ }
68
+ .toast i {
69
+ margin-right: 10px;
70
+ font-size: 1.2rem;
71
+ }
72
+ .channel-grid-small {
73
+ grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
74
+ }
75
+ .channel-grid-medium {
76
+ grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
77
+ }
78
+ .channel-grid-large {
79
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
80
+ }
81
+ .info-card {
82
+ background: linear-gradient(145deg, #1e293b, #0f172a);
83
+ border-radius: 10px;
84
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
85
+ transition: all 0.3s ease;
86
+ }
87
+ .info-card:hover {
88
+ transform: translateY(-2px);
89
+ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
90
+ }
91
+ .info-label {
92
+ color: #94a3b8;
93
+ font-size: 0.75rem;
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.05em;
96
+ }
97
+ .info-value {
98
+ color: #e2e8f0;
99
+ font-weight: 600;
100
+ }
101
+ .progress-bar {
102
+ height: 4px;
103
+ background: #334155;
104
+ border-radius: 2px;
105
+ overflow: hidden;
106
+ }
107
+ .progress-fill {
108
+ height: 100%;
109
+ background: #3b82f6;
110
+ width: 0%;
111
+ transition: width 0.3s ease;
112
+ }
113
+ .dropdown {
114
+ position: relative;
115
+ display: inline-block;
116
+ width: 100%;
117
+ }
118
+ .dropdown-content {
119
+ display: none;
120
+ position: absolute;
121
+ background-color: #1e293b;
122
+ min-width: 200px;
123
+ box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
124
+ z-index: 1;
125
+ border-radius: 0.5rem;
126
+ max-height: 300px;
127
+ overflow-y: auto;
128
+ }
129
+ .dropdown-content a {
130
+ color: #e2e8f0;
131
+ padding: 12px 16px;
132
+ text-decoration: none;
133
+ display: block;
134
+ transition: background-color 0.3s;
135
+ }
136
+ .dropdown-content a:hover {
137
+ background-color: #334155;
138
+ }
139
+ .dropdown:hover .dropdown-content {
140
+ display: block;
141
+ }
142
+ .dropdown-btn {
143
+ width: 100%;
144
+ text-align: left;
145
+ padding: 0.75rem 1rem;
146
+ background-color: #1e293b;
147
+ border: 1px solid #334155;
148
+ border-radius: 0.375rem;
149
+ color: #e2e8f0;
150
+ cursor: pointer;
151
+ display: flex;
152
+ justify-content: space-between;
153
+ align-items: center;
154
+ }
155
+ .dropdown-btn:hover {
156
+ background-color: #334155;
157
+ }
158
+ .dropdown-btn:after {
159
+ content: "▼";
160
+ font-size: 0.6rem;
161
+ margin-left: 10px;
162
+ }
163
+ .channel-list-container {
164
+ max-height: calc(100vh - 400px);
165
+ overflow-y: auto;
166
+ }
167
+ @media (max-width: 768px) {
168
+ .flex-col-reverse-mobile {
169
+ flex-direction: column-reverse;
170
+ }
171
+ .channel-list-container {
172
+ max-height: 50vh;
173
+ }
174
+ .player-container, .channels-container {
175
+ width: 100% !important;
176
+ }
177
+ }
178
+ .copy-btn {
179
+ transition: all 0.3s ease;
180
+ }
181
+ .copy-btn:hover {
182
+ transform: scale(1.05);
183
+ background-color: #3b82f6;
184
+ }
185
+ .copy-btn.copied {
186
+ background-color: #10b981;
187
+ }
188
+ </style>
189
+ </head>
190
+ <body class="bg-gray-900 text-white">
191
+ <div class="container mx-auto px-4 py-8">
192
+ <h1 class="text-3xl font-bold text-center mb-8">IPTV Player Premium</h1>
193
+
194
+ <!-- Pestañas de selección de fuente -->
195
+ <div class="flex mb-6 bg-gray-800 rounded-lg p-1">
196
+ <button id="localTab" class="source-tab active flex-1 py-2 px-4 rounded-lg font-medium">
197
+ <i class="fas fa-file-upload mr-2"></i>Cargar lista local
198
+ </button>
199
+ <button id="urlTab" class="source-tab flex-1 py-2 px-4 rounded-lg font-medium">
200
+ <i class="fas fa-link mr-2"></i>Cargar desde URL
201
+ </button>
202
+ <button id="sampleTab" class="source-tab flex-1 py-2 px-4 rounded-lg font-medium">
203
+ <i class="fas fa-tv mr-2"></i>Lista de ejemplo
204
+ </button>
205
+ </div>
206
+
207
+ <!-- Carga de archivo local -->
208
+ <div id="localSource" class="mb-6 p-4 bg-gray-800 rounded-lg">
209
+ <div class="flex items-center">
210
+ <input type="file" id="m3uFile" accept=".m3u,.m3u8" class="hidden">
211
+ <button id="selectFileBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded mr-3">
212
+ <i class="fas fa-folder-open mr-2"></i>Seleccionar archivo M3U
213
+ </button>
214
+ <span id="fileName" class="text-gray-400">No se ha seleccionado ningún archivo</span>
215
+ </div>
216
+ <div id="fileError" class="text-red-500 mt-2 hidden"></div>
217
+ </div>
218
+
219
+ <!-- Carga desde URL -->
220
+ <div id="urlSource" class="mb-6 p-4 bg-gray-800 rounded-lg hidden">
221
+ <div class="flex">
222
+ <input type="text" id="m3uUrl" placeholder="https://ejemplo.com/lista.m3u" class="flex-1 p-2 rounded-l bg-gray-700 text-white">
223
+ <button id="loadUrlBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-r">
224
+ <i class="fas fa-download mr-2"></i>Cargar
225
+ </button>
226
+ </div>
227
+ <div id="urlError" class="text-red-500 mt-2 hidden"></div>
228
+ </div>
229
+
230
+ <div class="flex flex-col lg:flex-row flex-col-reverse-mobile gap-8">
231
+ <!-- Columna izquierda - Reproductor -->
232
+ <div class="w-full lg:w-1/2 player-container">
233
+ <div class="bg-gray-800 rounded-lg p-4 h-full">
234
+ <div id="videoPlayer" class="w-full aspect-video rounded-lg mb-4 flex items-center justify-center">
235
+ <div id="playerPlaceholder" class="text-center p-4">
236
+ <i class="fas fa-tv text-4xl mb-2 text-gray-500"></i>
237
+ <p class="text-gray-400">Selecciona un canal para comenzar</p>
238
+ </div>
239
+ <video id="videoElement" controls class="w-full h-full hidden"></video>
240
+ </div>
241
+
242
+ <div id="nowPlaying" class="hidden">
243
+ <div class="flex items-center mb-2">
244
+ <img id="nowPlayingLogo" src="" alt="Logo" class="w-12 h-12 rounded mr-3">
245
+ <div>
246
+ <h2 id="nowPlayingName" class="font-bold text-lg"></h2>
247
+ <p id="nowPlayingGroup" class="text-gray-400 text-sm"></p>
248
+ </div>
249
+ </div>
250
+ <div class="flex items-center justify-between">
251
+ <div class="flex space-x-2">
252
+ <button id="playPauseBtn" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-1 rounded">
253
+ <i class="fas fa-play"></i>
254
+ </button>
255
+ <button id="stopBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-1 rounded">
256
+ <i class="fas fa-stop"></i>
257
+ </button>
258
+ <button id="fullscreenBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-1 rounded">
259
+ <i class="fas fa-expand"></i>
260
+ </button>
261
+ <button id="volumeBtn" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-1 rounded">
262
+ <i class="fas fa-volume-up"></i>
263
+ </button>
264
+ <input id="volumeControl" type="range" min="0" max="1" step="0.1" value="1" class="w-20">
265
+ </div>
266
+ <div id="loadingIndicator" class="hidden">
267
+ <span class="loading-spinner"></span>
268
+ <span class="ml-2">Cargando...</span>
269
+ </div>
270
+ </div>
271
+ </div>
272
+
273
+ <!-- Información del stream - Versión mejorada -->
274
+ <div id="streamInfo" class="mt-4 hidden">
275
+ <h3 class="font-bold mb-3 text-lg flex items-center">
276
+ <i class="fas fa-info-circle mr-2 text-blue-400"></i>
277
+ Información del stream
278
+ </h3>
279
+
280
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
281
+ <!-- Tarjeta de información básica -->
282
+ <div class="info-card p-4">
283
+ <div class="grid grid-cols-2 gap-4 mb-3">
284
+ <div>
285
+ <div class="info-label">Formato</div>
286
+ <div id="streamFormat" class="info-value">-</div>
287
+ </div>
288
+ <div>
289
+ <div class="info-label">Protocolo</div>
290
+ <div id="streamProtocol" class="info-value">-</div>
291
+ </div>
292
+ <div>
293
+ <div class="info-label">Estado</div>
294
+ <div id="streamStatus" class="info-value">-</div>
295
+ </div>
296
+ <div>
297
+ <div class="info-label">Resolución</div>
298
+ <div id="streamResolution" class="info-value">-</div>
299
+ </div>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- Tarjeta de tiempo de reproducción -->
304
+ <div class="info-card p-4">
305
+ <div class="mb-2">
306
+ <div class="info-label">Tiempo de reproducción</div>
307
+ <div id="streamDuration" class="info-value">00:00:00</div>
308
+ </div>
309
+ <div class="progress-bar">
310
+ <div id="progressFill" class="progress-fill"></div>
311
+ </div>
312
+ <div class="flex justify-between text-xs text-gray-400 mt-1">
313
+ <span id="currentTime">00:00:00</span>
314
+ <span id="totalTime">00:00:00</span>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
319
+ <!-- URL del canal con botón de copiar -->
320
+ <div class="info-card p-4 mt-4 relative">
321
+ <div class="info-label">URL del canal</div>
322
+ <div id="streamUrl" class="info-value truncate pr-10">-</div>
323
+ <button id="copyUrlBtn" class="copy-btn absolute right-4 top-8 bg-gray-600 hover:bg-gray-700 text-white p-2 rounded">
324
+ <i class="fas fa-copy"></i>
325
+ </button>
326
+ </div>
327
+ </div>
328
+ </div>
329
+ </div>
330
+
331
+ <!-- Columna derecha - Canales -->
332
+ <div class="w-full lg:w-1/2 channels-container">
333
+ <!-- Controles superiores -->
334
+ <div class="flex flex-col md:flex-row gap-4 mb-4">
335
+ <!-- Selector de categorías -->
336
+ <div class="w-full md:w-1/2">
337
+ <div class="dropdown">
338
+ <button class="dropdown-btn">
339
+ <span id="currentCategoryText">Seleccionar categoría</span>
340
+ </button>
341
+ <div id="categoryDropdown" class="dropdown-content">
342
+ <!-- Las categorías se cargarán aquí dinámicamente -->
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <!-- Selector de tamaño -->
348
+ <div class="w-full md:w-1/2">
349
+ <select id="iconSizeSelector" class="w-full bg-gray-700 text-white p-2 rounded">
350
+ <option value="small">Pequeño</option>
351
+ <option value="medium" selected>Mediano</option>
352
+ <option value="large">Grande</option>
353
+ </select>
354
+ </div>
355
+ </div>
356
+
357
+ <!-- Buscador y lista de canales -->
358
+ <div class="bg-gray-800 rounded-lg p-4 h-full">
359
+ <div class="mb-4 flex items-center">
360
+ <input type="text" id="searchInput" placeholder="Buscar canales..." class="flex-1 p-2 rounded bg-gray-700 text-white">
361
+ <button id="clearSearchBtn" class="ml-2 p-2 text-gray-400 hover:text-white">
362
+ <i class="fas fa-times"></i>
363
+ </button>
364
+ </div>
365
+
366
+ <div id="channelCount" class="text-sm text-gray-400 mb-3">0 canales cargados</div>
367
+
368
+ <div id="channelList" class="channel-list-container space-y-2 overflow-y-auto">
369
+ <div class="text-center text-gray-500 py-10">
370
+ <i class="fas fa-tv text-4xl mb-2"></i>
371
+ <p>Selecciona una categoría</p>
372
+ </div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+ </div>
377
+ </div>
378
+
379
+ <!-- Contenedor de notificaciones toast -->
380
+ <div id="toastContainer"></div>
381
+
382
+ <script>
383
+ // Esperar a que el DOM esté completamente cargado
384
+ document.addEventListener('DOMContentLoaded', function() {
385
+ // Datos de ejemplo de lista M3U
386
+ const sampleM3U = `#EXTM3U
387
+ #EXTINF:-1, tvg-id="" tvg-name="1 ACTUALIZA 20241005" group-title="GUIAS" tvg-logo="https://cdn.vectorstock.com/i/1000x1000/61/05/iptv-icon-ip-tv-video-channel-box-concept-vector-30766105.webp",1 ACTUALIZA 20241005
388
+ https://linear-46.frequency.stream/dist/localnow/46/hls/master/playlist.m3u8
389
+ #EXTINF:-1, tvg-id="" tvg-name="4KURD" group-title="ORIENTE" tvg-logo="",4KURD
390
+ http://51.210.199.23/hls/stream.m3u8
391
+ #EXTINF:-1, tvg-id="504TV.hn" tvg-name="504 TV" group-title="ENTRETENIMIENTO" tvg-logo="https://i.imgur.com/c2HLI3k.png",504 TV
392
+ https://mediacp.us:8081/504tvhn/index.m3u8
393
+ #EXTINF:-1, tvg-id="5DiasTV.py" tvg-name="5DIAS TV" group-title="GENERAL" tvg-logo="https://i.imgur.com/AX25tmv.png",5DIAS TV
394
+ https://ythls.armelin.one/channel/UCiyJDRTHlTOGn6Fc5_BxcJw.m3u8
395
+ #EXTINF:-1, tvg-id="ARCanal.pa" tvg-name="A&R CANAL" group-title="RELIGIOUS" tvg-logo="https://i.imgur.com/ejRJqSI.png",A&R CANAL
396
+ http://51.222.9.192:3589/stream/play.m3u8`;
397
+
398
+ // Elementos de la interfaz
399
+ const localTab = document.getElementById('localTab');
400
+ const urlTab = document.getElementById('urlTab');
401
+ const sampleTab = document.getElementById('sampleTab');
402
+ const localSource = document.getElementById('localSource');
403
+ const urlSource = document.getElementById('urlSource');
404
+ const m3uFileInput = document.getElementById('m3uFile');
405
+ const selectFileBtn = document.getElementById('selectFileBtn');
406
+ const fileName = document.getElementById('fileName');
407
+ const fileError = document.getElementById('fileError');
408
+ const m3uUrl = document.getElementById('m3uUrl');
409
+ const loadUrlBtn = document.getElementById('loadUrlBtn');
410
+ const urlError = document.getElementById('urlError');
411
+ const videoElement = document.getElementById('videoElement');
412
+ const playerPlaceholder = document.getElementById('playerPlaceholder');
413
+ const nowPlayingSection = document.getElementById('nowPlaying');
414
+ const nowPlayingName = document.getElementById('nowPlayingName');
415
+ const nowPlayingGroup = document.getElementById('nowPlayingGroup');
416
+ const nowPlayingLogo = document.getElementById('nowPlayingLogo');
417
+ const playPauseBtn = document.getElementById('playPauseBtn');
418
+ const stopBtn = document.getElementById('stopBtn');
419
+ const fullscreenBtn = document.getElementById('fullscreenBtn');
420
+ const volumeBtn = document.getElementById('volumeBtn');
421
+ const volumeControl = document.getElementById('volumeControl');
422
+ const loadingIndicator = document.getElementById('loadingIndicator');
423
+ const searchInput = document.getElementById('searchInput');
424
+ const clearSearchBtn = document.getElementById('clearSearchBtn');
425
+ const channelList = document.getElementById('channelList');
426
+ const channelCount = document.getElementById('channelCount');
427
+ const categoryDropdown = document.getElementById('categoryDropdown');
428
+ const currentCategoryText = document.getElementById('currentCategoryText');
429
+ const iconSizeSelector = document.getElementById('iconSizeSelector');
430
+ const streamInfo = document.getElementById('streamInfo');
431
+ const streamFormat = document.getElementById('streamFormat');
432
+ const streamProtocol = document.getElementById('streamProtocol');
433
+ const streamStatus = document.getElementById('streamStatus');
434
+ const streamResolution = document.getElementById('streamResolution');
435
+ const streamUrl = document.getElementById('streamUrl');
436
+ const streamDuration = document.getElementById('streamDuration');
437
+ const currentTime = document.getElementById('currentTime');
438
+ const totalTime = document.getElementById('totalTime');
439
+ const progressFill = document.getElementById('progressFill');
440
+ const toastContainer = document.getElementById('toastContainer');
441
+ const copyUrlBtn = document.getElementById('copyUrlBtn');
442
+
443
+ // Variables de estado
444
+ let hls = null;
445
+ let currentChannel = null;
446
+ let channels = [];
447
+ let groupedChannels = {};
448
+ let currentCategory = null;
449
+ let iconSize = 'medium'; // Tamaño de icono por defecto
450
+ let durationInterval = null;
451
+
452
+ // Configurar eventos de las pestañas
453
+ localTab.addEventListener('click', () => switchTab('local'));
454
+ urlTab.addEventListener('click', () => switchTab('url'));
455
+ sampleTab.addEventListener('click', () => {
456
+ switchTab('sample');
457
+ loadM3UContent(sampleM3U);
458
+ showToast('Lista de ejemplo cargada', 'success');
459
+ });
460
+
461
+ // Función para cambiar entre pestañas
462
+ function switchTab(tab) {
463
+ // Remover clase active de todas las pestañas
464
+ localTab.classList.remove('active');
465
+ urlTab.classList.remove('active');
466
+ sampleTab.classList.remove('active');
467
+
468
+ // Ocultar todos los paneles de fuente
469
+ localSource.classList.add('hidden');
470
+ urlSource.classList.add('hidden');
471
+
472
+ // Activar la pestaña seleccionada
473
+ if (tab === 'local') {
474
+ localTab.classList.add('active');
475
+ localSource.classList.remove('hidden');
476
+ } else if (tab === 'url') {
477
+ urlTab.classList.add('active');
478
+ urlSource.classList.remove('hidden');
479
+ } else if (tab === 'sample') {
480
+ sampleTab.classList.add('active');
481
+ }
482
+ }
483
+
484
+ // Configurar evento para seleccionar archivo
485
+ selectFileBtn.addEventListener('click', () => m3uFileInput.click());
486
+
487
+ // Configurar evento cuando se selecciona un archivo
488
+ m3uFileInput.addEventListener('change', function() {
489
+ if (this.files.length > 0) {
490
+ const file = this.files[0];
491
+ fileName.textContent = file.name;
492
+ fileError.classList.add('hidden');
493
+
494
+ const reader = new FileReader();
495
+ reader.onload = function(e) {
496
+ loadM3UContent(e.target.result);
497
+ showToast('Archivo cargado correctamente', 'success');
498
+ };
499
+ reader.onerror = function() {
500
+ showToast('Error al leer el archivo', 'error');
501
+ fileError.textContent = 'Error al leer el archivo';
502
+ fileError.classList.remove('hidden');
503
+ };
504
+ reader.readAsText(file);
505
+ }
506
+ });
507
+
508
+ // Configurar evento para cargar desde URL
509
+ loadUrlBtn.addEventListener('click', function() {
510
+ const url = m3uUrl.value.trim();
511
+ if (!url) {
512
+ showToast('Ingresa una URL válida', 'error');
513
+ urlError.textContent = 'Por favor ingresa una URL válida';
514
+ urlError.classList.remove('hidden');
515
+ return;
516
+ }
517
+
518
+ urlError.classList.add('hidden');
519
+ loadingIndicator.classList.remove('hidden');
520
+
521
+ // Usar proxy CORS para evitar problemas de CORS
522
+ const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`;
523
+
524
+ fetch(proxyUrl)
525
+ .then(response => {
526
+ if (!response.ok) throw new Error('Error al cargar la lista');
527
+ return response.json();
528
+ })
529
+ .then(data => {
530
+ if (data.contents) {
531
+ loadM3UContent(data.contents);
532
+ showToast('Lista cargada desde URL', 'success');
533
+ } else {
534
+ throw new Error('No se pudo obtener el contenido');
535
+ }
536
+ })
537
+ .catch(error => {
538
+ showToast('Error al cargar la URL', 'error');
539
+ urlError.textContent = error.message;
540
+ urlError.classList.remove('hidden');
541
+ })
542
+ .finally(() => {
543
+ loadingIndicator.classList.add('hidden');
544
+ });
545
+ });
546
+
547
+ // Configurar selector de tamaño de iconos
548
+ iconSizeSelector.addEventListener('change', function() {
549
+ iconSize = this.value;
550
+ updateChannelDisplay();
551
+ });
552
+
553
+ // Configurar botón para copiar URL
554
+ copyUrlBtn.addEventListener('click', function() {
555
+ if (streamUrl.textContent && streamUrl.textContent !== '-') {
556
+ navigator.clipboard.writeText(streamUrl.textContent)
557
+ .then(() => {
558
+ const btn = this;
559
+ btn.innerHTML = '<i class="fas fa-check"></i>';
560
+ btn.classList.add('copied');
561
+ showToast('URL copiada al portapapeles', 'success');
562
+
563
+ setTimeout(() => {
564
+ btn.innerHTML = '<i class="fas fa-copy"></i>';
565
+ btn.classList.remove('copied');
566
+ }, 2000);
567
+ })
568
+ .catch(err => {
569
+ showToast('Error al copiar la URL', 'error');
570
+ console.error('Error al copiar: ', err);
571
+ });
572
+ }
573
+ });
574
+
575
+ // Función para mostrar notificaciones toast
576
+ function showToast(message, type = 'info') {
577
+ const toast = document.createElement('div');
578
+ toast.className = `toast ${type}`;
579
+
580
+ let icon = 'fa-info-circle';
581
+ if (type === 'error') icon = 'fa-exclamation-circle';
582
+ if (type === 'success') icon = 'fa-check-circle';
583
+ if (type === 'warning') icon = 'fa-exclamation-triangle';
584
+
585
+ toast.innerHTML = `<i class="fas ${icon}"></i> ${message}`;
586
+ toastContainer.appendChild(toast);
587
+
588
+ // Mostrar toast
589
+ setTimeout(() => {
590
+ toast.classList.add('show');
591
+ }, 10);
592
+
593
+ // Ocultar toast después de 3 segundos
594
+ setTimeout(() => {
595
+ toast.classList.remove('show');
596
+ setTimeout(() => {
597
+ toast.remove();
598
+ }, 300);
599
+ }, 3000);
600
+ }
601
+
602
+ // Función para cargar contenido M3U
603
+ function loadM3UContent(content) {
604
+ channels = parseM3U(content);
605
+ groupedChannels = groupChannelsByCategory(channels);
606
+ displayCategories(Object.keys(groupedChannels));
607
+ channelCount.textContent = `${channels.length} canales cargados`;
608
+ }
609
+
610
+ // Función para buscar canales
611
+ searchInput.addEventListener('input', function() {
612
+ const searchTerm = this.value.toLowerCase();
613
+ const channelItems = document.querySelectorAll('.channel-item');
614
+ let visibleCount = 0;
615
+
616
+ channelItems.forEach(item => {
617
+ const channelName = item.querySelector('.channel-name').textContent.toLowerCase();
618
+ if (channelName.includes(searchTerm)) {
619
+ item.classList.remove('hidden');
620
+ visibleCount++;
621
+ } else {
622
+ item.classList.add('hidden');
623
+ }
624
+ });
625
+
626
+ // Actualizar contador de canales visibles
627
+ channelCount.textContent = `${visibleCount} de ${groupedChannels[currentCategory]?.length || 0} canales`;
628
+ });
629
+
630
+ // Función para limpiar búsqueda
631
+ clearSearchBtn.addEventListener('click', function() {
632
+ searchInput.value = '';
633
+ const channelItems = document.querySelectorAll('.channel-item');
634
+ channelItems.forEach(item => item.classList.remove('hidden'));
635
+ if (currentCategory) {
636
+ channelCount.textContent = `${groupedChannels[currentCategory]?.length || 0} canales`;
637
+ } else {
638
+ channelCount.textContent = `${channels.length} canales cargados`;
639
+ }
640
+ });
641
+
642
+ // Configurar botón play/pause
643
+ playPauseBtn.addEventListener('click', function() {
644
+ if (videoElement.paused) {
645
+ videoElement.play();
646
+ playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
647
+ } else {
648
+ videoElement.pause();
649
+ playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
650
+ }
651
+ });
652
+
653
+ // Configurar botón stop
654
+ stopBtn.addEventListener('click', function() {
655
+ stopPlayer();
656
+ });
657
+
658
+ // Configurar botón pantalla completa
659
+ fullscreenBtn.addEventListener('click', function() {
660
+ if (videoElement.requestFullscreen) {
661
+ videoElement.requestFullscreen();
662
+ } else if (videoElement.webkitRequestFullscreen) {
663
+ videoElement.webkitRequestFullscreen();
664
+ } else if (videoElement.msRequestFullscreen) {
665
+ videoElement.msRequestFullscreen();
666
+ }
667
+ });
668
+
669
+ // Configurar control de volumen
670
+ volumeBtn.addEventListener('click', function() {
671
+ videoElement.muted = !videoElement.muted;
672
+ volumeBtn.innerHTML = videoElement.muted ?
673
+ '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
674
+ });
675
+
676
+ volumeControl.addEventListener('input', function() {
677
+ videoElement.volume = this.value;
678
+ if (this.value == 0) {
679
+ volumeBtn.innerHTML = '<i class="fas fa-volume-mute"></i>';
680
+ } else {
681
+ volumeBtn.innerHTML = '<i class="fas fa-volume-up"></i>';
682
+ }
683
+ });
684
+
685
+ // Función para reproducir un canal
686
+ function playChannel(channel) {
687
+ currentChannel = channel;
688
+
689
+ // Actualizar la interfaz
690
+ playerPlaceholder.classList.add('hidden');
691
+ videoElement.classList.remove('hidden');
692
+ nowPlayingSection.classList.remove('hidden');
693
+ streamInfo.classList.remove('hidden');
694
+ nowPlayingName.textContent = channel.name;
695
+ nowPlayingGroup.textContent = channel.group;
696
+ nowPlayingLogo.src = channel.logo || 'https://cdn.vectorstock.com/i/1000x1000/61/05/iptv-icon-ip-tv-video-channel-box-concept-vector-30766105.webp';
697
+ nowPlayingLogo.onerror = function() {
698
+ this.src = 'https://cdn.vectorstock.com/i/1000x1000/61/05/iptv-icon-ip-tv-video-channel-box-concept-vector-30766105.webp';
699
+ };
700
+
701
+ // Mostrar URL del canal
702
+ streamUrl.textContent = channel.url;
703
+
704
+ // Mostrar indicador de carga
705
+ loadingIndicator.classList.remove('hidden');
706
+
707
+ // Detener cualquier reproducción anterior
708
+ if (hls) {
709
+ hls.destroy();
710
+ }
711
+
712
+ // Limpiar intervalo de duración si existe
713
+ if (durationInterval) {
714
+ clearInterval(durationInterval);
715
+ }
716
+
717
+ videoElement.pause();
718
+ videoElement.src = '';
719
+
720
+ // Detectar formato y protocolo del stream
721
+ const format = detectFormat(channel.url);
722
+ const protocol = detectProtocol(channel.url);
723
+
724
+ streamFormat.textContent = format;
725
+ streamProtocol.textContent = protocol;
726
+ streamStatus.textContent = 'Cargando...';
727
+ streamResolution.textContent = '-';
728
+
729
+ // Verificar si se necesita HLS.js
730
+ if (format === 'HLS') {
731
+ if (Hls.isSupported()) {
732
+ hls = new Hls();
733
+ hls.loadSource(channel.url);
734
+ hls.attachMedia(videoElement);
735
+ hls.on(Hls.Events.MANIFEST_PARSED, function() {
736
+ videoElement.play();
737
+ playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
738
+ loadingIndicator.classList.add('hidden');
739
+ streamStatus.textContent = 'Reproduciendo';
740
+
741
+ // Obtener información de resolución
742
+ const levels = hls.levels;
743
+ if (levels && levels.length > 0) {
744
+ const level = levels[hls.currentLevel];
745
+ if (level) {
746
+ streamResolution.textContent = `${level.width}x${level.height}`;
747
+ }
748
+ }
749
+
750
+ // Iniciar actualización de tiempo
751
+ startDurationTracking();
752
+ });
753
+ hls.on(Hls.Events.ERROR, function(event, data) {
754
+ console.error('Error HLS:', data);
755
+ streamStatus.textContent = 'Error';
756
+ if (data.fatal) {
757
+ switch(data.type) {
758
+ case Hls.ErrorTypes.NETWORK_ERROR:
759
+ showToast('Error de red. Intenta nuevamente.', 'error');
760
+ break;
761
+ case Hls.ErrorTypes.MEDIA_ERROR:
762
+ showToast('Error de medio. Intenta con otro canal.', 'error');
763
+ break;
764
+ default:
765
+ showToast('Error al reproducir el canal.', 'error');
766
+ }
767
+ loadingIndicator.classList.add('hidden');
768
+ }
769
+ });
770
+ } else if (videoElement.canPlayType('application/vnd.apple.mpegurl')) {
771
+ // Soporte nativo de HLS (Safari)
772
+ videoElement.src = channel.url;
773
+ videoElement.addEventListener('loadedmetadata', function() {
774
+ videoElement.play();
775
+ playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
776
+ loadingIndicator.classList.add('hidden');
777
+ streamStatus.textContent = 'Reproduciendo';
778
+
779
+ // Para HLS nativo, no podemos obtener fácilmente la resolución
780
+ streamResolution.textContent = `${videoElement.videoWidth}x${videoElement.videoHeight}`;
781
+
782
+ // Iniciar actualización de tiempo
783
+ startDurationTracking();
784
+ });
785
+ } else {
786
+ showToast('Tu navegador no soporta la reproducción de HLS.', 'error');
787
+ loadingIndicator.classList.add('hidden');
788
+ streamStatus.textContent = 'No soportado';
789
+ }
790
+ } else {
791
+ // Reproducción directa para MP4, AVI, etc.
792
+ videoElement.src = channel.url;
793
+ videoElement.addEventListener('loadedmetadata', function() {
794
+ videoElement.play();
795
+ playPauseBtn.innerHTML = '<i class="fas fa-pause"></i>';
796
+ loadingIndicator.classList.add('hidden');
797
+ streamStatus.textContent = 'Reproduciendo';
798
+ streamResolution.textContent = `${videoElement.videoWidth}x${videoElement.videoHeight}`;
799
+
800
+ // Iniciar actualización de tiempo
801
+ startDurationTracking();
802
+ });
803
+ }
804
+
805
+ // Manejar errores
806
+ videoElement.addEventListener('error', function() {
807
+ loadingIndicator.classList.add('hidden');
808
+ streamStatus.textContent = 'Error';
809
+ showToast('Error al reproducir el canal. Intenta con otro.', 'error');
810
+ });
811
+ }
812
+
813
+ // Función para iniciar el seguimiento del tiempo de reproducción
814
+ function startDurationTracking() {
815
+ // Limpiar intervalo anterior si existe
816
+ if (durationInterval) {
817
+ clearInterval(durationInterval);
818
+ }
819
+
820
+ // Actualizar tiempo total
821
+ const updateTotalTime = () => {
822
+ if (videoElement.duration) {
823
+ totalTime.textContent = formatTime(videoElement.duration);
824
+ }
825
+ };
826
+
827
+ // Actualizar tiempo actual y progreso
828
+ const updateCurrentTime = () => {
829
+ if (!isNaN(videoElement.duration)) {
830
+ const current = videoElement.currentTime;
831
+ const duration = videoElement.duration;
832
+ const percent = (current / duration) * 100;
833
+
834
+ currentTime.textContent = formatTime(current);
835
+ progressFill.style.width = `${percent}%`;
836
+
837
+ // Calcular tiempo transcurrido
838
+ const elapsed = new Date().getTime() - startTime;
839
+ streamDuration.textContent = formatTime(elapsed / 1000);
840
+ }
841
+ };
842
+
843
+ let startTime = new Date().getTime();
844
+
845
+ // Actualizar inmediatamente
846
+ updateTotalTime();
847
+ updateCurrentTime();
848
+
849
+ // Configurar intervalo para actualizar cada segundo
850
+ durationInterval = setInterval(() => {
851
+ updateCurrentTime();
852
+ }, 1000);
853
+
854
+ // Actualizar cuando cambia la duración (puede cambiar en streams en vivo)
855
+ videoElement.addEventListener('durationchange', updateTotalTime);
856
+ }
857
+
858
+ // Función para formatear tiempo (segundos a HH:MM:SS)
859
+ function formatTime(seconds) {
860
+ const date = new Date(0);
861
+ date.setSeconds(seconds);
862
+ return date.toISOString().substr(11, 8);
863
+ }
864
+
865
+ // Función para detectar formato de video desde URL
866
+ function detectFormat(url) {
867
+ if (url.includes('.m3u8')) return 'HLS';
868
+ if (url.includes('.mp4')) return 'MP4';
869
+ if (url.includes('.avi')) return 'AVI';
870
+ if (url.includes('.mkv')) return 'MKV';
871
+ if (url.includes('.flv')) return 'FLV';
872
+ if (url.includes('.mov')) return 'MOV';
873
+ return 'Desconocido';
874
+ }
875
+
876
+ // Función para detectar protocolo desde URL
877
+ function detectProtocol(url) {
878
+ if (url.startsWith('http://')) return 'HTTP';
879
+ if (url.startsWith('https://')) return 'HTTPS';
880
+ if (url.startsWith('rtmp://')) return 'RTMP';
881
+ if (url.startsWith('rtsp://')) return 'RTSP';
882
+ if (url.startsWith('udp://')) return 'UDP';
883
+ return 'Desconocido';
884
+ }
885
+
886
+ // Función para detener el reproductor
887
+ function stopPlayer() {
888
+ if (hls) {
889
+ hls.destroy();
890
+ hls = null;
891
+ }
892
+
893
+ // Limpiar intervalo de duración
894
+ if (durationInterval) {
895
+ clearInterval(durationInterval);
896
+ durationInterval = null;
897
+ }
898
+
899
+ videoElement.pause();
900
+ videoElement.src = '';
901
+ videoElement.classList.add('hidden');
902
+ playerPlaceholder.classList.remove('hidden');
903
+ nowPlayingSection.classList.add('hidden');
904
+ streamInfo.classList.add('hidden');
905
+ playPauseBtn.innerHTML = '<i class="fas fa-play"></i>';
906
+
907
+ // Resetear información de tiempo
908
+ currentTime.textContent = '00:00:00';
909
+ totalTime.textContent = '00:00:00';
910
+ progressFill.style.width = '0%';
911
+ streamDuration.textContent = '00:00:00';
912
+ }
913
+
914
+ // Función para parsear contenido M3U
915
+ function parseM3U(content) {
916
+ const lines = content.split('\n');
917
+ const channels = [];
918
+ let currentChannel = {};
919
+
920
+ for (let i = 0; i < lines.length; i++) {
921
+ const line = lines[i].trim();
922
+
923
+ if (line.startsWith('#EXTINF:')) {
924
+ // Parsear línea EXTINF
925
+ const parts = line.split(',');
926
+ const infoPart = parts[0];
927
+ const namePart = parts.length > 1 ? parts[1] : '';
928
+
929
+ // Extraer atributos
930
+ const attrs = {};
931
+ const attrMatches = infoPart.matchAll(/(\S+?)="(.*?)"/g);
932
+ for (const match of attrMatches) {
933
+ attrs[match[1]] = match[2];
934
+ }
935
+
936
+ // Crear objeto de canal - usando tvg-name para nombre y group-title para categoría
937
+ currentChannel = {
938
+ name: attrs['tvg-name'] || namePart, // Preferir tvg-name, usar nombre después de la coma como alternativa
939
+ group: attrs['group-title'] || 'Sin categoría',
940
+ logo: attrs['tvg-logo'] || '',
941
+ url: ''
942
+ };
943
+ } else if (line && !line.startsWith('#') && currentChannel.name) {
944
+ // Esta es la línea de URL
945
+ currentChannel.url = line;
946
+ channels.push(currentChannel);
947
+ currentChannel = {};
948
+ }
949
+ }
950
+
951
+ return channels;
952
+ }
953
+
954
+ // Función para agrupar canales por categoría
955
+ function groupChannelsByCategory(channels) {
956
+ const groups = {};
957
+
958
+ channels.forEach(channel => {
959
+ if (!groups[channel.group]) {
960
+ groups[channel.group] = [];
961
+ }
962
+ groups[channel.group].push(channel);
963
+ });
964
+
965
+ return groups;
966
+ }
967
+
968
+ // Función para mostrar categorías en el selector desplegable
969
+ function displayCategories(categories) {
970
+ categoryDropdown.innerHTML = '';
971
+
972
+ if (categories.length === 0) {
973
+ categoryDropdown.innerHTML = `
974
+ <a class="text-gray-500">
975
+ <i class="fas fa-exclamation-triangle mr-2"></i>
976
+ No se encontraron categorías
977
+ </a>
978
+ `;
979
+ return;
980
+ }
981
+
982
+ // Ordenar categorías alfabéticamente
983
+ categories.sort();
984
+
985
+ categories.forEach(category => {
986
+ const categoryItem = document.createElement('a');
987
+ categoryItem.href = '#';
988
+ categoryItem.textContent = category;
989
+ categoryItem.addEventListener('click', (e) => {
990
+ e.preventDefault();
991
+ selectCategory(category);
992
+ currentCategoryText.textContent = category;
993
+ });
994
+
995
+ categoryDropdown.appendChild(categoryItem);
996
+ });
997
+
998
+ // Seleccionar primera categoría por defecto
999
+ if (categories.length > 0) {
1000
+ selectCategory(categories[0]);
1001
+ currentCategoryText.textContent = categories[0];
1002
+ }
1003
+ }
1004
+
1005
+ // Función para seleccionar una categoría y mostrar sus canales
1006
+ function selectCategory(category) {
1007
+ currentCategory = category;
1008
+ updateChannelDisplay();
1009
+ }
1010
+
1011
+ // Función para actualizar la visualización de canales según categoría y tamaño de icono
1012
+ function updateChannelDisplay() {
1013
+ if (!currentCategory || !groupedChannels[currentCategory]) {
1014
+ channelList.innerHTML = `
1015
+ <div class="text-center text-gray-500 py-10">
1016
+ <i class="fas fa-tv text-4xl mb-2"></i>
1017
+ <p>Selecciona una categoría</p>
1018
+ </div>
1019
+ `;
1020
+ return;
1021
+ }
1022
+
1023
+ const channels = groupedChannels[currentCategory];
1024
+ channelList.innerHTML = '';
1025
+ channelCount.textContent = `${channels.length} canales`;
1026
+
1027
+ // Crear contenedor grid con clase apropiada según tamaño de icono
1028
+ const gridContainer = document.createElement('div');
1029
+ gridContainer.className = `grid gap-4 ${getGridClass()}`;
1030
+
1031
+ // Ordenar canales alfabéticamente
1032
+ channels.sort((a, b) => a.name.localeCompare(b.name));
1033
+
1034
+ channels.forEach(channel => {
1035
+ const channelItem = document.createElement('div');
1036
+ channelItem.className = 'channel-item bg-gray-700 rounded-lg p-3 cursor-pointer hover:bg-gray-600';
1037
+ channelItem.addEventListener('click', () => playChannel(channel));
1038
+
1039
+ const channelLogo = document.createElement('img');
1040
+ channelLogo.src = channel.logo || 'https://cdn.vectorstock.com/i/1000x1000/61/05/iptv-icon-ip-tv-video-channel-box-concept-vector-30766105.webp';
1041
+ channelLogo.alt = channel.name;
1042
+ channelLogo.className = `w-full ${getIconSizeClass()} object-contain mb-2 rounded`;
1043
+ channelLogo.onerror = function() {
1044
+ this.src = 'https://cdn.vectorstock.com/i/1000x1000/61/05/iptv-icon-ip-tv-video-channel-box-concept-vector-30766105.webp';
1045
+ };
1046
+
1047
+ const channelName = document.createElement('div');
1048
+ channelName.className = 'channel-name text-center text-sm font-medium truncate';
1049
+ channelName.textContent = channel.name;
1050
+
1051
+ channelItem.appendChild(channelLogo);
1052
+ channelItem.appendChild(channelName);
1053
+ gridContainer.appendChild(channelItem);
1054
+ });
1055
+
1056
+ channelList.appendChild(gridContainer);
1057
+ }
1058
+
1059
+ // Función para obtener clase grid apropiada según tamaño de icono
1060
+ function getGridClass() {
1061
+ switch(iconSize) {
1062
+ case 'small': return 'channel-grid-small';
1063
+ case 'medium': return 'channel-grid-medium';
1064
+ case 'large': return 'channel-grid-large';
1065
+ default: return 'channel-grid-medium';
1066
+ }
1067
+ }
1068
+
1069
+ // Función para obtener clase de tamaño de icono según selección
1070
+ function getIconSizeClass() {
1071
+ switch(iconSize) {
1072
+ case 'small': return 'h-16';
1073
+ case 'medium': return 'h-24';
1074
+ case 'large': return 'h-32';
1075
+ default: return 'h-24';
1076
+ }
1077
+ }
1078
+
1079
+ // Inicializar con pestaña local activa
1080
+ switchTab('local');
1081
+ });
1082
+ </script>
1083
+ <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=titanito/iptv-player-premium" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
1084
+ </html>