Rubentnoda commited on
Commit
9c943d9
·
verified ·
1 Parent(s): ae25a43

Upload folder using huggingface_hub

Browse files
Files changed (2) hide show
  1. app.py +793 -0
  2. requirements.txt +10 -0
app.py ADDED
@@ -0,0 +1,793 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import time
3
+ import json
4
+ from typing import Dict, List, Tuple, Optional
5
+ import threading
6
+ import asyncio
7
+ import logging
8
+ from dataclasses import dataclass
9
+ from enum import Enum
10
+
11
+ # For real Chromecast control
12
+ try:
13
+ import pychromecast
14
+ from pychromecast.controllers.youtube import YouTubeController
15
+ from pychromecast.controllers.media import MediaController
16
+ from pychromecast import Chromecast
17
+ CHROMECAST_AVAILABLE = True
18
+ except ImportError:
19
+ CHROMECAST_AVAILABLE = False
20
+
21
+ # Configure logging
22
+ logging.basicConfig(level=logging.INFO)
23
+ logger = logging.getLogger(__name__)
24
+
25
+ class DeviceStatus(Enum):
26
+ DISCONNECTED = "disconnected"
27
+ CONNECTED = "connected"
28
+ PLAYING = "playing"
29
+ PAUSED = "paused"
30
+ ERROR = "error"
31
+
32
+ @dataclass
33
+ class TVDevice:
34
+ """Represents a Chromecast TV device"""
35
+ id: int
36
+ name: str
37
+ chromecast: Optional[Chromecast] = None
38
+ status: DeviceStatus = DeviceStatus.DISCONNECTED
39
+ current_content: str = "Esperando..."
40
+ volume: int = 50
41
+ is_muted: bool = False
42
+ media_controller: Optional[MediaController] = None
43
+
44
+ class ChromecastController:
45
+ """Real Chromecast controller with device management"""
46
+
47
+ def __init__(self):
48
+ self.devices: Dict[int, TVDevice] = {}
49
+ self.current_video_url: Optional[str] = None
50
+ self.progress: float = 0
51
+ self.playlist: List[Dict] = []
52
+ self.discovered_casts: List[Chromecast] = []
53
+ self.is_playing: bool = False
54
+ self.progress_thread: Optional[threading.Thread] = None
55
+ self.stop_progress = False
56
+
57
+ # Initialize demo devices
58
+ self._initialize_demo_devices()
59
+
60
+ # Discover real devices if available
61
+ if CHROMECAST_AVAILABLE:
62
+ self._discover_devices()
63
+
64
+ # Initialize default playlist
65
+ self._initialize_playlist()
66
+
67
+ def _initialize_demo_devices(self):
68
+ """Initialize demo TV devices"""
69
+ demo_configs = [
70
+ {"name": "TV - Sala Principal", "location": "Sala"},
71
+ {"name": "TV - Barra", "location": "Bar"},
72
+ {"name": "TV - Terraza", "location": "Terraza"},
73
+ ]
74
+
75
+ for i, config in enumerate(demo_configs, 1):
76
+ self.devices[i] = TVDevice(
77
+ id=i,
78
+ name=config["name"]
79
+ )
80
+
81
+ def _discover_devices(self):
82
+ """Discover real Chromecast devices on network"""
83
+ try:
84
+ # Discover Chromecasts (blocking operation)
85
+ self.discovered_casts, browser = pychromecast.get_listed_chromecasts()
86
+ logger.info(f"Found {len(self.discovered_casts)} Chromecast devices")
87
+
88
+ # Map discovered devices to our demo devices
89
+ for i, cast in enumerate(self.discovered_casts[:3], 1):
90
+ if i in self.devices:
91
+ self.devices[i].chromecast = cast
92
+ self.devices[i].status = DeviceStatus.CONNECTED
93
+ cast.wait()
94
+
95
+ # Get media controller
96
+ self.devices[i].media_controller = cast.media_controller
97
+
98
+ logger.info(f"Connected to {cast.name}")
99
+
100
+ except Exception as e:
101
+ logger.error(f"Error discovering devices: {e}")
102
+
103
+ def _initialize_playlist(self):
104
+ """Initialize default playlist"""
105
+ self.playlist = [
106
+ {
107
+ "title": "Video Promocional - Restaurante",
108
+ "source": "YouTube",
109
+ "url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
110
+ "duration": "3:45",
111
+ "thumbnail": "https://picsum.photos/seed/promo/60/40"
112
+ },
113
+ {
114
+ "title": "Video Corporativo 2024",
115
+ "source": "Vimeo",
116
+ "url": "https://vimeo.com/123456789",
117
+ "duration": "2:30",
118
+ "thumbnail": "https://picsum.photos/seed/corp/60/40"
119
+ },
120
+ {
121
+ "title": "Video Evento Especial",
122
+ "source": "Directo",
123
+ "url": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4",
124
+ "duration": "5:15",
125
+ "thumbnail": "https://picsum.photos/seed/event/60/40"
126
+ }
127
+ ]
128
+
129
+ def discover_new_devices(self) -> Tuple[str, str, str]:
130
+ """Discover new Chromecast devices"""
131
+ if not CHROMECAST_AVAILABLE:
132
+ return "❌ Error", "PyChromecast no está instalado", "error"
133
+
134
+ try:
135
+ self._discover_devices()
136
+ count = sum(1 for device in self.devices.values() if device.chromecast)
137
+ return "✅ Descubiertos", f"Se encontraron {count} dispositivos", "success"
138
+ except Exception as e:
139
+ return "❌ Error", f"Error al descubrir: {str(e)}", "error"
140
+
141
+ def get_device_status(self, device_id: int) -> Dict:
142
+ """Get status of a specific device"""
143
+ device = self.devices.get(device_id)
144
+ if not device:
145
+ return {}
146
+
147
+ # Update real device status if available
148
+ if device.chromecast and CHROMECAST_AVAILABLE:
149
+ try:
150
+ mc = device.chromecast.media_controller
151
+ if mc.status:
152
+ if mc.status.player_is_playing:
153
+ device.status = DeviceStatus.PLAYING
154
+ elif mc.status.player_is_paused:
155
+ device.status = DeviceStatus.PAUSED
156
+ else:
157
+ device.status = DeviceStatus.CONNECTED
158
+
159
+ device.current_content = mc.status.title or "Sin título"
160
+ device.volume = int(device.chromecast.status.volume_level * 100)
161
+ device.is_muted = device.chromecast.status.volume_muted
162
+ except Exception as e:
163
+ logger.error(f"Error updating device {device_id}: {e}")
164
+ device.status = DeviceStatus.ERROR
165
+
166
+ return {
167
+ "name": device.name,
168
+ "status": device.status.value,
169
+ "connected": device.chromecast is not None,
170
+ "playing": device.status == DeviceStatus.PLAYING,
171
+ "content": device.current_content,
172
+ "volume": device.volume,
173
+ "muted": device.is_muted,
174
+ "device_type": "Chromecast" if device.chromecast else "Simulado"
175
+ }
176
+
177
+ def get_all_devices_status(self) -> List[Dict]:
178
+ """Get status of all devices"""
179
+ return [self.get_device_status(i) for i in range(1, 4)]
180
+
181
+ def load_video(self, url: str, title: str = "") -> Tuple[str, str, str]:
182
+ """Load video to all connected devices"""
183
+ if not url:
184
+ return "❌ Error", "Por favor, introduce una URL válida", "error"
185
+
186
+ self.current_video_url = url
187
+ video_title = title or self._extract_video_name(url)
188
+
189
+ loaded_count = 0
190
+ errors = []
191
+
192
+ for device in self.devices.values():
193
+ if device.chromecast and CHROMECAST_AVAILABLE:
194
+ try:
195
+ mc = device.chromecast.media_controller
196
+
197
+ # Handle different video sources
198
+ if "youtube.com" in url:
199
+ # Extract YouTube video ID
200
+ video_id = url.split("v=")[-1].split("&")[0]
201
+ yt_controller = YouTubeController()
202
+ device.chromecast.register_handler(yt_controller)
203
+ yt_controller.play_video(video_id)
204
+ else:
205
+ # Load generic media
206
+ mc.play_media(url, "video/mp4", title=video_title)
207
+
208
+ device.current_content = video_title
209
+ loaded_count += 1
210
+
211
+ except Exception as e:
212
+ errors.append(f"{device.name}: {str(e)}")
213
+ else:
214
+ # Simulated device
215
+ device.current_content = video_title
216
+ device.status = DeviceStatus.CONNECTED
217
+
218
+ if loaded_count > 0:
219
+ return "✅ Cargado", f"Video cargado en {loaded_count} dispositivos", "success"
220
+ elif errors:
221
+ return "❌ Error", f"Errores: {'; '.join(errors[:2])}", "error"
222
+ else:
223
+ return "⚠️ Simulado", "Video cargado (modo simulación)", "info"
224
+
225
+ def _extract_video_name(self, url: str) -> str:
226
+ """Extract video name from URL"""
227
+ if "youtube.com" in url.lower():
228
+ return "Video de YouTube"
229
+ elif "vimeo.com" in url.lower():
230
+ return "Video de Vimeo"
231
+ elif ".mp4" in url.lower():
232
+ return "Video Directo MP4"
233
+ elif "commondatastorage.googleapis.com" in url.lower():
234
+ return "Video de Google Storage"
235
+ else:
236
+ return "Video Online"
237
+
238
+ def control_device(self, device_id: int, action: str) -> Tuple[str, str, str]:
239
+ """Control individual device"""
240
+ device = self.devices.get(device_id)
241
+ if not device:
242
+ return "❌ Error", f"Dispositivo {device_id} no encontrado", "error"
243
+
244
+ if not device.chromecast or not CHROMECAST_AVAILABLE:
245
+ # Simulated control
246
+ if action in ["play", "pause", "stop", "mute"]:
247
+ device.status = DeviceStatus.PLAYING if action == "play" else DeviceStatus.CONNECTED
248
+ if action == "mute":
249
+ device.is_muted = not device.is_muted
250
+ return "⚠️ Simulado", f"Acción '{action}' simulada en {device.name}", "info"
251
+ return "❌ Error", "Dispositivo no disponible", "error"
252
+
253
+ try:
254
+ mc = device.chromecast.media_controller
255
+
256
+ if action == "play":
257
+ if not self.current_video_url:
258
+ return "❌ Error", "No hay video cargado", "error"
259
+ mc.play()
260
+ device.status = DeviceStatus.PLAYING
261
+ return "✅ Reproduciendo", f"{device.name} está reproduciendo", "success"
262
+
263
+ elif action == "pause":
264
+ mc.pause()
265
+ device.status = DeviceStatus.PAUSED
266
+ return "⏸️ Pausado", f"{device.name} pausado", "info"
267
+
268
+ elif action == "stop":
269
+ mc.stop()
270
+ device.status = DeviceStatus.CONNECTED
271
+ device.current_content = "Esperando..."
272
+ return "⏹️ Detenido", f"{device.name} detenido", "info"
273
+
274
+ elif action == "mute":
275
+ device.chromecast.set_volume_muted(not device.is_muted)
276
+ device.is_muted = not device.is_muted
277
+ status = "silenciado" if device.is_muted else "activado"
278
+ return "🔇 Volumen", f"{device.name} {status}", "info"
279
+
280
+ elif action == "volume_up":
281
+ new_volume = min(100, device.volume + 10)
282
+ device.chromecast.set_volume(new_volume / 100)
283
+ device.volume = new_volume
284
+ return "🔊 Volumen", f"{device.name}: {new_volume}%", "info"
285
+
286
+ elif action == "volume_down":
287
+ new_volume = max(0, device.volume - 10)
288
+ device.chromecast.set_volume(new_volume / 100)
289
+ device.volume = new_volume
290
+ return "🔊 Volumen", f"{device.name}: {new_volume}%", "info"
291
+
292
+ except Exception as e:
293
+ return "❌ Error", f"Error en {device.name}: {str(e)}", "error"
294
+
295
+ return "❌ Error", "Acción no reconocida", "error"
296
+
297
+ def control_all_devices(self, action: str) -> Tuple[str, str, str]:
298
+ """Control all devices simultaneously"""
299
+ results = []
300
+ success_count = 0
301
+
302
+ for device_id in self.devices.keys():
303
+ title, message, status = self.control_device(device_id, action)
304
+ results.append(f"{device_id}: {message}")
305
+ if status == "success":
306
+ success_count += 1
307
+
308
+ if action == "stop":
309
+ self.current_video_url = None
310
+ self.progress = 0
311
+ self.is_playing = False
312
+
313
+ if success_count > 0:
314
+ return f"✅ {action.title()}", f"Acción ejecutada en {success_count} dispositivos", "success"
315
+ else:
316
+ return "⚠️ Simulado", "Acción ejecutada (modo simulación)", "info"
317
+
318
+ def set_volume_all(self, volume: int) -> Tuple[str, str, str]:
319
+ """Set volume for all devices"""
320
+ volume = max(0, min(100, volume))
321
+ success_count = 0
322
+
323
+ for device in self.devices.values():
324
+ if device.chromecast and CHROMECAST_AVAILABLE:
325
+ try:
326
+ device.chromecast.set_volume(volume / 100)
327
+ device.volume = volume
328
+ success_count += 1
329
+ except Exception as e:
330
+ logger.error(f"Error setting volume on {device.name}: {e}")
331
+ else:
332
+ device.volume = volume
333
+
334
+ return "🔊 Volumen", f"Volumen ajustado a {volume}% ({success_count} reales)", "info"
335
+
336
+ def update_progress(self) -> Tuple[float, str, str]:
337
+ """Update playback progress"""
338
+ if self.is_playing and self.progress < 100:
339
+ self.progress += 0.5
340
+
341
+ # Get real progress from first connected device
342
+ for device in self.devices.values():
343
+ if device.chromecast and CHROMECAST_AVAILABLE:
344
+ try:
345
+ mc = device.chromecast.media_controller
346
+ if mc.status and mc.status.current_time and mc.status.duration:
347
+ real_progress = (mc.status.current_time / mc.status.duration) * 100
348
+ current_seconds = int(mc.status.current_time)
349
+ duration_seconds = int(mc.status.duration)
350
+ current_time = f"{current_seconds // 60:02d}:{current_seconds % 60:02d}"
351
+ duration = f"{duration_seconds // 60:02d}:{duration_seconds % 60:02d}"
352
+ return real_progress, current_time, duration
353
+ except:
354
+ pass
355
+
356
+ # Fallback to simulated progress
357
+ current_seconds = int(self.progress * 2.25)
358
+ current_time = f"{current_seconds // 60:02d}:{current_seconds % 60:02d}"
359
+ duration = "03:45"
360
+
361
+ return self.progress, current_time, duration
362
+
363
+ def add_to_playlist(self, url: str, title: str = "") -> Tuple[str, str, str]:
364
+ """Add video to playlist"""
365
+ if not url:
366
+ return "❌ Error", "Introduce una URL primero", "error"
367
+
368
+ if not title:
369
+ title = self._extract_video_name(url)
370
+
371
+ self.playlist.append({
372
+ "title": title,
373
+ "source": "Custom",
374
+ "url": url,
375
+ "duration": "--:--",
376
+ "thumbnail": f"https://picsum.photos/seed/{title}/60/40"
377
+ })
378
+
379
+ return "✅ Éxito", f"'{title}' agregado a la playlist", "success"
380
+
381
+ def play_from_playlist(self, index: int) -> Tuple[str, str, str]:
382
+ """Play video from playlist"""
383
+ if 0 <= index < len(self.playlist):
384
+ video = self.playlist[index]
385
+ return self.load_video(video["url"], video["title"])
386
+
387
+ return "❌ Error", "Video no encontrado en playlist", "error"
388
+
389
+ def get_playlist(self) -> List[Dict]:
390
+ """Get current playlist"""
391
+ return self.playlist
392
+
393
+ # Initialize controller
394
+ controller = ChromecastController()
395
+
396
+ def update_devices_display():
397
+ """Update the devices status display"""
398
+ statuses = controller.get_all_devices_status()
399
+ html = ""
400
+
401
+ for i, status in enumerate(statuses, 1):
402
+ status_colors = {
403
+ "connected": "#16a34a",
404
+ "playing": "#d97706",
405
+ "paused": "#2563eb",
406
+ "disconnected": "#dc2626",
407
+ "error": "#dc2626"
408
+ }
409
+
410
+ status_texts = {
411
+ "connected": "Conectado",
412
+ "playing": "Reproduciendo",
413
+ "paused": "Pausado",
414
+ "disconnected": "Desconectado",
415
+ "error": "Error"
416
+ }
417
+
418
+ status_color = status_colors.get(status["status"], "#6b7280")
419
+ status_text = status_texts.get(status["status"], "Desconocido")
420
+
421
+ # Status icon
422
+ status_icons = {
423
+ "playing": "🎬",
424
+ "connected": "✅",
425
+ "paused": "⏸️",
426
+ "disconnected": "❌",
427
+ "error": "⚠️"
428
+ }
429
+
430
+ icon = status_icons.get(status["status"], "📺")
431
+
432
+ html += f"""
433
+ <div style="background: white; border-radius: 12px; padding: 16px; margin: 8px 0; box-shadow: 0 2px 8px rgba(0,0,0,0.1); border-left: 4px solid {status_color};">
434
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
435
+ <h3 style="margin: 0; display: flex; align-items: center; gap: 8px;">
436
+ {icon} {status['name']}
437
+ </h3>
438
+ <div style="display: flex; gap: 8px; align-items: center;">
439
+ <span style="background: {status_color}20; color: {status_color}; padding: 4px 12px; border-radius: 8px; font-size: 12px; font-weight: 600;">
440
+ {status_text}
441
+ </span>
442
+ <span style="background: #f3f4f6; color: #6b7280; padding: 4px 8px; border-radius: 6px; font-size: 11px;">
443
+ {status['device_type']}
444
+ </span>
445
+ </div>
446
+ </div>
447
+ <div style="display: grid; gap: 8px;">
448
+ <div style="display: flex; justify-content: space-between; padding: 8px; background: #f9fafb; border-radius: 6px;">
449
+ <span style="font-weight: 500; color: #6b7280;">Reproduciendo:</span>
450
+ <span style="font-weight: 500;" id="tv{i}-content">{status['content']}</span>
451
+ </div>
452
+ <div style="display: flex; justify-content: space-between; padding: 8px; background: #f9fafb; border-radius: 6px;">
453
+ <span style="font-weight: 500; color: #6b7280;">Volumen:</span>
454
+ <span style="font-weight: 500;" id="tv{i}-volume">{status['volume']}%{' 🔇' if status['muted'] else ''}</span>
455
+ </div>
456
+ </div>
457
+ </div>
458
+ """
459
+
460
+ return html
461
+
462
+ def update_playlist_display():
463
+ """Update the playlist display"""
464
+ playlist = controller.get_playlist()
465
+ html = ""
466
+
467
+ for i, video in enumerate(playlist):
468
+ html += f"""
469
+ <div style="background: #f9fafb; border-radius: 8px; padding: 12px; margin: 4px 0; cursor: pointer; transition: all 0.2s; border: 1px solid #e5e7eb;"
470
+ onmouseover="this.style.background='#f3f4f6'; this.style.borderColor='#d1d5db'"
471
+ onmouseout="this.style.background='#f9fafb'; this.style.borderColor='#e5e7eb'">
472
+ <div style="display: flex; justify-content: space-between; align-items: center;">
473
+ <div style="display: flex; align-items: center; gap: 12px;">
474
+ <img src="{video.get('thumbnail', 'https://picsum.photos/seed/video/60/40')}" alt="Thumbnail" style="width: 60px; height: 40px; border-radius: 4px; object-fit: cover;">
475
+ <div>
476
+ <h4 style="margin: 0; font-size: 14px; font-weight: 600;">{video['title']}</h4>
477
+ <p style="margin: 0; font-size: 12px; color: #6b7280;">{video['source']} • {video['duration']}</p>
478
+ </div>
479
+ </div>
480
+ <div style="display: flex; gap: 4px;">
481
+ <button onclick="play_playlist_item({i})" style="background: #10b981; color: white; border: none; padding: 6px 12px; border-radius: 6px; cursor: pointer; font-size: 12px;">▶️</button>
482
+ </div>
483
+ </div>
484
+ </div>
485
+ """
486
+
487
+ return html
488
+
489
+ def load_video_url(url):
490
+ """Load video from URL"""
491
+ if not url.strip():
492
+ return "❌ Error", "Introduce una URL válida", update_devices_display(), ""
493
+
494
+ title, message, status = controller.load_video(url)
495
+ return title, message, update_devices_display(), url
496
+
497
+ def discover_devices():
498
+ """Discover new devices"""
499
+ title, message, status = controller.discover_new_devices()
500
+ return title, message, update_devices_display()
501
+
502
+ def control_individual_device(device_id, action):
503
+ """Control individual device"""
504
+ title, message, status = controller.control_device(device_id, action)
505
+ return title, message, update_devices_display()
506
+
507
+ def control_all_devices_action(action):
508
+ """Control all devices"""
509
+ title, message, status = controller.control_all_devices(action)
510
+ return title, message, update_devices_display()
511
+
512
+ def change_volume_all(volume):
513
+ """Change volume for all devices"""
514
+ title, message, status = controller.set_volume_all(volume)
515
+ return title, message, update_devices_display(), f"{volume}%"
516
+
517
+ def update_playback_progress():
518
+ """Update playback progress"""
519
+ progress, current_time, duration = controller.update_progress()
520
+ return progress, current_time, duration
521
+
522
+ def add_video_to_playlist(url, title):
523
+ """Add video to playlist"""
524
+ result_title, message, status = controller.add_to_playlist(url, title)
525
+ return result_title, message, update_playlist_display()
526
+
527
+ def play_playlist_item(index):
528
+ """Play item from playlist"""
529
+ title, message, status = controller.play_from_playlist(index)
530
+ return title, message, update_devices_display()
531
+
532
+ def load_sample_video(sample_type):
533
+ """Load sample video"""
534
+ samples = {
535
+ "youtube": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
536
+ "vimeo": "https://vimeo.com/123456789",
537
+ "direct": "https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
538
+ }
539
+
540
+ url = samples.get(sample_type, "")
541
+ return load_video_url(url)
542
+
543
+ # Create CSS for better styling
544
+ custom_css = """
545
+ .container {
546
+ max-width: 1400px;
547
+ margin: 0 auto;
548
+ }
549
+
550
+ .status-card {
551
+ transition: all 0.3s ease;
552
+ }
553
+
554
+ .status-card:hover {
555
+ transform: translateY(-2px);
556
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
557
+ }
558
+
559
+ .device-button {
560
+ transition: all 0.2s ease;
561
+ }
562
+
563
+ .device-button:hover {
564
+ transform: scale(1.05);
565
+ }
566
+
567
+ .playlist-item {
568
+ transition: all 0.2s ease;
569
+ }
570
+
571
+ .progress-bar {
572
+ height: 8px;
573
+ }
574
+
575
+ .gradio-container {
576
+ background: #f8fafc;
577
+ }
578
+ """
579
+
580
+ # Create Gradio interface
581
+ with gr.Blocks(title="Control Multimedia Chromecast - Real", css=custom_css, theme=gr.themes.Soft()) as demo:
582
+ gr.HTML("""
583
+ <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 24px; border-radius: 16px; margin-bottom: 24px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
584
+ <h1 style="margin: 0; display: flex; align-items: center; gap: 12px; font-size: 28px;">
585
+ 📺 Control Multimedia Chromecast - Hostelería
586
+ </h1>
587
+ <p style="margin: 8px 0 0 0; opacity: 0.95; font-size: 16px;">
588
+ Sistema de control real para dispositivos Chromecast con descubrimiento automático
589
+ </p>
590
+ <div style="margin-top: 12px; display: flex; gap: 8px; align-items: center;">
591
+ <span style="background: rgba(255,255,255,0.2); padding: 4px 8px; border-radius: 4px; font-size: 12px;">
592
+ {'🟢 Real' if CHROMECAST_AVAILABLE else '🟡 Modo Demo'}
593
+ </span>
594
+ <span style="background: rgba(255,255,255,0.2); padding: 4px 8px; border-radius: 4px; font-size: 12px;">
595
+ Python + PyChromecast
596
+ </span>
597
+ </div>
598
+ </div>
599
+ """)
600
+
601
+ with gr.Row():
602
+ with gr.Column(scale=2):
603
+ # Device Discovery Section
604
+ with gr.Group():
605
+ gr.HTML("<h2>🔍 Descubrimiento de Dispositivos</h2>")
606
+ with gr.Row():
607
+ discover_btn = gr.Button("🔍 Descubrir Chromecasts", variant="primary")
608
+ refresh_status_btn = gr.Button("🔄 Actualizar Estado", variant="secondary")
609
+
610
+ # Quick Actions
611
+ gr.HTML("<h2>⚡ Acciones Rápidas</h2>")
612
+ with gr.Row():
613
+ play_all_btn = gr.Button("▶️ Reproducir Todo", variant="primary", size="sm")
614
+ pause_all_btn = gr.Button("⏸️ Pausar Todo", variant="secondary", size="sm")
615
+ stop_all_btn = gr.Button("⏹️ Detener Todo", variant="stop", size="sm")
616
+ mute_all_btn = gr.Button("🔇 Silenciar Todo", variant="secondary", size="sm")
617
+
618
+ # Device Status Display
619
+ gr.HTML("<h2>📺 Estado de los Dispositivos</h2>")
620
+ devices_display = gr.HTML(update_devices_display())
621
+
622
+ # Video Input Section
623
+ gr.HTML("<h2>🎬 Control de Reproducción</h2>")
624
+ with gr.Row():
625
+ video_url = gr.Textbox(
626
+ label="URL del Video",
627
+ placeholder="https://www.youtube.com/watch?v=...",
628
+ scale=3
629
+ )
630
+ load_btn = gr.Button("📥 Cargar Video", variant="primary", scale=1)
631
+
632
+ with gr.Row():
633
+ youtube_btn = gr.Button("📺 YouTube Sample", size="sm")
634
+ vimeo_btn = gr.Button("🎥 Vimeo Sample", size="sm")
635
+ direct_btn = gr.Button("🔗 Video Directo", size="sm")
636
+
637
+ # Progress Bar
638
+ gr.HTML("<h3>📊 Progreso de Reproducción</h3>")
639
+ progress = gr.Slider(0, 100, value=0, label="Progreso", interactive=True)
640
+ with gr.Row():
641
+ current_time = gr.Textbox("00:00", label="Tiempo Actual", interactive=False, scale=1)
642
+ duration_time = gr.Textbox("03:45", label="Duración Total", interactive=False, scale=1)
643
+
644
+ # Media Controls
645
+ gr.HTML("<h3>🎮 Controles de Multimedia</h3>")
646
+ with gr.Row():
647
+ rewind_btn = gr.Button("⏪ -10s", size="sm")
648
+ play_btn = gr.Button("▶️ Reproducir", variant="primary", size="sm")
649
+ pause_btn = gr.Button("⏸️ Pausar", variant="secondary", size="sm")
650
+ stop_btn = gr.Button("⏹️ Detener", variant="stop", size="sm")
651
+ forward_btn = gr.Button("⏩ +10s", size="sm")
652
+
653
+ # Volume Control
654
+ gr.HTML("<h3>🔊 Control de Volumen Maestro</h3>")
655
+ with gr.Row():
656
+ volume_slider = gr.Slider(0, 100, value=50, label="Volumen", scale=2)
657
+ volume_percentage = gr.Textbox("50%", label="Porcentaje", interactive=False, scale=1)
658
+
659
+ with gr.Column(scale=1):
660
+ # Individual Device Controls
661
+ gr.HTML("<h2>🎛️ Controles Individuales</h2>")
662
+
663
+ for i in range(1, 4):
664
+ with gr.Group():
665
+ device_name = f"TV {i}"
666
+ gr.HTML(f"<h4 style='margin: 8px 0;'>📺 {device_name}</h4>")
667
+ with gr.Row():
668
+ play_btn = gr.Button("▶️", size="sm", variant="primary", elem_classes=["device-button"])
669
+ pause_btn = gr.Button("⏸️", size="sm", elem_classes=["device-button"])
670
+ stop_btn = gr.Button("⏹️", size="sm", variant="stop", elem_classes=["device-button"])
671
+ mute_btn = gr.Button("🔇", size="sm", elem_classes=["device-button"])
672
+ vol_up_btn = gr.Button("🔊", size="sm", elem_classes=["device-button"])
673
+ vol_down_btn = gr.Button("🔉", size="sm", elem_classes=["device-button"])
674
+
675
+ # Store buttons in variables for event binding
676
+ if i == 1:
677
+ tv1_play, tv1_pause, tv1_stop, tv1_mute, tv1_vol_up, tv1_vol_down = play_btn, pause_btn, stop_btn, mute_btn, vol_up_btn, vol_down_btn
678
+ elif i == 2:
679
+ tv2_play, tv2_pause, tv2_stop, tv2_mute, tv2_vol_up, tv2_vol_down = play_btn, pause_btn, stop_btn, mute_btn, vol_up_btn, vol_down_btn
680
+ else:
681
+ tv3_play, tv3_pause, tv3_stop, tv3_mute, tv3_vol_up, tv3_vol_down = play_btn, pause_btn, stop_btn, mute_btn, vol_up_btn, vol_down_btn
682
+
683
+ # Playlist Section
684
+ gr.HTML("<h2>📋 Playlist de Videos</h2>")
685
+ playlist_display = gr.HTML(update_playlist_display())
686
+
687
+ with gr.Group():
688
+ playlist_url = gr.Textbox(label="URL para Playlist", placeholder="Agregar video a playlist")
689
+ playlist_title = gr.Textbox(label="Título (opcional)")
690
+ with gr.Row():
691
+ add_playlist_btn = gr.Button("➕ Agregar", variant="primary")
692
+ clear_playlist_btn = gr.Button("🗑️ Limpiar", variant="secondary")
693
+
694
+ # Status Display
695
+ with gr.Row():
696
+ status_title = gr.Textbox("✅ Sistema Listo", label="Estado", interactive=False, scale=1)
697
+ status_message = gr.Textbox("Esperando acción", label="Mensaje", interactive=False, scale=2)
698
+
699
+ # Event Handlers
700
+ load_btn.click(load_video_url, inputs=[video_url], outputs=[status_title, status_message, devices_display, video_url])
701
+
702
+ # Device discovery
703
+ discover_btn.click(discover_devices, outputs=[status_title, status_message, devices_display])
704
+ refresh_status_btn.click(lambda: update_devices_display(), outputs=[devices_display])
705
+
706
+ # Sample videos
707
+ youtube_btn.click(fn=lambda: load_sample_video("youtube"), outputs=[status_title, status_message, devices_display, video_url])
708
+ vimeo_btn.click(fn=lambda: load_sample_video("vimeo"), outputs=[status_title, status_message, devices_display, video_url])
709
+ direct_btn.click(fn=lambda: load_sample_video("direct"), outputs=[status_title, status_message, devices_display, video_url])
710
+
711
+ # Quick actions
712
+ play_all_btn.click(fn=lambda: control_all_devices_action("play"), outputs=[status_title, status_message, devices_display])
713
+ pause_all_btn.click(fn=lambda: control_all_devices_action("pause"), outputs=[status_title, status_message, devices_display])
714
+ stop_all_btn.click(fn=lambda: control_all_devices_action("stop"), outputs=[status_title, status_message, devices_display])
715
+ mute_all_btn.click(fn=lambda: control_all_devices_action("mute"), outputs=[status_title, status_message, devices_display])
716
+
717
+ # Media controls
718
+ play_btn.click(fn=lambda: control_all_devices_action("play"), outputs=[status_title, status_message, devices_display])
719
+ pause_btn.click(fn=lambda: control_all_devices_action("pause"), outputs=[status_title, status_message, devices_display])
720
+ stop_btn.click(fn=lambda: control_all_devices_action("stop"), outputs=[status_title, status_message, devices_display])
721
+
722
+ # Volume control
723
+ volume_slider.change(change_volume_all, inputs=[volume_slider], outputs=[status_title, status_message, devices_display, volume_percentage])
724
+
725
+ # Individual TV controls - TV 1
726
+ tv1_play.click(fn=lambda: control_individual_device(1, "play"), outputs=[status_title, status_message, devices_display])
727
+ tv1_pause.click(fn=lambda: control_individual_device(1, "pause"), outputs=[status_title, status_message, devices_display])
728
+ tv1_stop.click(fn=lambda: control_individual_device(1, "stop"), outputs=[status_title, status_message, devices_display])
729
+ tv1_mute.click(fn=lambda: control_individual_device(1, "mute"), outputs=[status_title, status_message, devices_display])
730
+ tv1_vol_up.click(fn=lambda: control_individual_device(1, "volume_up"), outputs=[status_title, status_message, devices_display])
731
+ tv1_vol_down.click(fn=lambda: control_individual_device(1, "volume_down"), outputs=[status_title, status_message, devices_display])
732
+
733
+ # Individual TV controls - TV 2
734
+ tv2_play.click(fn=lambda: control_individual_device(2, "play"), outputs=[status_title, status_message, devices_display])
735
+ tv2_pause.click(fn=lambda: control_individual_device(2, "pause"), outputs=[status_title, status_message, devices_display])
736
+ tv2_stop.click(fn=lambda: control_individual_device(2, "stop"), outputs=[status_title, status_message, devices_display])
737
+ tv2_mute.click(fn=lambda: control_individual_device(2, "mute"), outputs=[status_title, status_message, devices_display])
738
+ tv2_vol_up.click(fn=lambda: control_individual_device(2, "volume_up"), outputs=[status_title, status_message, devices_display])
739
+ tv2_vol_down.click(fn=lambda: control_individual_device(2, "volume_down"), outputs=[status_title, status_message, devices_display])
740
+
741
+ # Individual TV controls - TV 3
742
+ tv3_play.click(fn=lambda: control_individual_device(3, "play"), outputs=[status_title, status_message, devices_display])
743
+ tv3_pause.click(fn=lambda: control_individual_device(3, "pause"), outputs=[status_title, status_message, devices_display])
744
+ tv3_stop.click(fn=lambda: control_individual_device(3, "stop"), outputs=[status_title, status_message, devices_display])
745
+ tv3_mute.click(fn=lambda: control_individual_device(3, "mute"), outputs=[status_title, status_message, devices_display])
746
+ tv3_vol_up.click(fn=lambda: control_individual_device(3, "volume_up"), outputs=[status_title, status_message, devices_display])
747
+ tv3_vol_down.click(fn=lambda: control_individual_device(3, "volume_down"), outputs=[status_title, status_message, devices_display])
748
+
749
+ # Playlist controls
750
+ add_playlist_btn.click(add_video_to_playlist, inputs=[playlist_url, playlist_title], outputs=[status_title, status_message, playlist_display])
751
+
752
+ # Timer for progress update
753
+ timer = gr.Timer(1.0)
754
+ timer.tick(update_playback_progress, outputs=[progress, current_time, duration_time])
755
+
756
+ # Footer with information
757
+ gr.HTML("""
758
+ <div style="text-align: center; margin-top: 30px; padding: 20px; border-top: 1px solid #e5e7eb; background: white; border-radius: 12px;">
759
+ <div style="margin-bottom: 12px;">
760
+ <span style="background: #10b981; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 8px;">
761
+ Motor: Python 3.9+
762
+ </span>
763
+ <span style="background: #3b82f6; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px; margin-right: 8px;">
764
+ Librería: PyChromecast
765
+ </span>
766
+ <span style="background: #8b5cf6; color: white; padding: 4px 8px; border-radius: 4px; font-size: 12px;">
767
+ Protocolo: Cast v2
768
+ </span>
769
+ </div>
770
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="color: #3b82f6; text-decoration: none; font-weight: 600;">
771
+ Built with anycoder
772
+ </a>
773
+ </div>
774
+ """)
775
+
776
+ if __name__ == "__main__":
777
+ # Display installation info if PyChromecast is not available
778
+ if not CHROMECAST_AVAILABLE:
779
+ print("\n" + "="*60)
780
+ print("⚠️ ADVERTENCIA: PyChromecast no está instalado")
781
+ print("Para control real de Chromecasts, instala:")
782
+ print("pip install pychromecast")
783
+ print("="*60 + "\n")
784
+
785
+ print("🚀 Iniciando Control Multimedia Chromecast...")
786
+ print(f"📊 Modo: {'Real (con Chromecasts)' if CHROMECAST_AVAILABLE else 'Demo/Simulación'}")
787
+
788
+ demo.launch(
789
+ theme=gr.themes.Soft(),
790
+ share=False,
791
+ server_name="0.0.0.0",
792
+ server_port=7860
793
+ )
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ pychromecast
2
+ gradio
3
+ requests
4
+ protobuf
5
+ zeroconf
6
+ Pillow
7
+ numpy
8
+ pandas
9
+ matplotlib
10
+ plotly