from nicegui import ui import os import threading import json import time import yt_dlp # --- CONFIGURAÇÕES --- DOWNLOAD_DIR = "/downloads/Youtube" STATUS_FILE = "/app/data/dl_status.json" # --- UTILITÁRIOS --- def save_status(data): try: with open(STATUS_FILE, 'w') as f: json.dump(data, f) except: pass def load_status(): if not os.path.exists(STATUS_FILE): return None try: with open(STATUS_FILE, 'r') as f: return json.load(f) except: return None # --- WORKER (BACKEND) --- class DownloadWorker(threading.Thread): def __init__(self, url, format_type): super().__init__() self.url = url self.format_type = format_type self.daemon = True self.stop_requested = False def progress_hook(self, d): if self.stop_requested: raise Exception("Cancelado pelo usuário") if d['status'] == 'downloading': total = d.get('total_bytes') or d.get('total_bytes_estimate') or 0 downloaded = d.get('downloaded_bytes', 0) pct = int((downloaded / total) * 100) if total > 0 else 0 speed = d.get('speed', 0) or 0 speed_str = f"{speed / 1024 / 1024:.2f} MiB/s" filename = os.path.basename(d.get('filename', 'Baixando...')) save_status({ "running": True, "file": filename, "progress": pct, "log": f"Baixando: {speed_str} | {d.get('_eta_str', '?')} restantes", "stop_requested": False }) elif d['status'] == 'finished': save_status({ "running": True, "file": "Processando...", "progress": 99, "log": "Convertendo/Juntando arquivos...", "stop_requested": False }) def run(self): if not os.path.exists(DOWNLOAD_DIR): os.makedirs(DOWNLOAD_DIR, exist_ok=True) ydl_opts = { 'outtmpl': f'{DOWNLOAD_DIR}/%(title)s.%(ext)s', 'progress_hooks': [self.progress_hook], 'nocheckcertificate': True, 'ignoreerrors': True, 'ffmpeg_location': '/usr/bin/ffmpeg' } if self.format_type == 'best': ydl_opts['format'] = 'bestvideo+bestaudio/best' ydl_opts['merge_output_format'] = 'mkv' elif self.format_type == 'audio': ydl_opts['format'] = 'bestaudio/best' ydl_opts['postprocessors'] = [{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3','preferredquality': '192'}] elif self.format_type == '1080p': ydl_opts['format'] = 'bestvideo[height<=1080]+bestaudio/best[height<=1080]' ydl_opts['merge_output_format'] = 'mkv' try: save_status({"running": True, "file": "Iniciando...", "progress": 0, "log": "Conectando..."}) with yt_dlp.YoutubeDL(ydl_opts) as ydl: ydl.download([self.url]) save_status({"running": False, "file": "Concluído!", "progress": 100, "log": "Sucesso."}) except Exception as e: msg = "Cancelado." if "Cancelado" in str(e) else str(e) save_status({"running": False, "file": "Parado", "progress": 0, "log": msg}) # --- INTERFACE (FRONTEND) --- class DownloaderInterface: def __init__(self): self.container = None self.timer = None self.btn_download = None self.card_status = None # Elementos dinâmicos self.lbl_file = None self.progress = None self.lbl_log = None self.btn_stop = None def start_download(self, url, fmt): if not url: ui.notify('Cole uma URL!', type='warning') return if os.path.exists(STATUS_FILE): os.remove(STATUS_FILE) t = DownloadWorker(url, fmt) t.start() ui.notify('Iniciando...') self.render_update() def stop_download(self): data = load_status() if data: data['stop_requested'] = True save_status(data) ui.notify('Parando...') def render(self): ui.label('📺 YouTube Downloader').classes('text-xl font-bold mb-2') # --- INPUT --- with ui.card().classes('w-full p-4 mb-4'): url_input = ui.input('URL do Vídeo').classes('w-full').props('clearable placeholder="https://youtube.com/..."') with ui.row().classes('items-center mt-2'): fmt_select = ui.select( {'best': 'Melhor Qualidade (MKV)', '1080p': 'Limitado a 1080p (MKV)', 'audio': 'Apenas Áudio (MP3)'}, value='best', label='Formato' ).classes('w-64') self.btn_download = ui.button('Baixar', on_click=lambda: self.start_download(url_input.value, fmt_select.value))\ .props('icon=download color=primary') # --- MONITORAMENTO --- # CORREÇÃO AQUI: Criamos o card primeiro, depois definimos visibilidade self.card_status = ui.card().classes('w-full p-4') self.card_status.visible = False # Esconde inicialmente with self.card_status: ui.label('Progresso').classes('font-bold') self.lbl_file = ui.label('Aguardando...') self.progress = ui.linear_progress(value=0).classes('w-full') self.lbl_log = ui.label('---').classes('text-sm text-gray-500 font-mono') with ui.row().classes('w-full justify-end mt-2'): self.btn_stop = ui.button('🛑 Cancelar', on_click=self.stop_download).props('color=red flat') self.timer = ui.timer(1.0, self.render_update) def render_update(self): data = load_status() if not data: if self.card_status: self.card_status.visible = False if self.btn_download: self.btn_download.enable() return # Atualiza UI is_running = data.get('running', False) if self.btn_download: if is_running: self.btn_download.disable() else: self.btn_download.enable() if self.card_status: self.card_status.visible = True if self.lbl_file: self.lbl_file.text = f"Arquivo: {data.get('file', '?')}" if self.progress: self.progress.value = data.get('progress', 0) / 100 if self.lbl_log: self.lbl_log.text = data.get('log', '') if self.btn_stop: self.btn_stop.visible = is_running # --- INICIALIZADOR --- def create_ui(): dl = DownloaderInterface() dl.container = ui.column().classes('w-full h-full p-4 gap-4') with dl.container: dl.render()