186 lines
6.6 KiB
Python
186 lines
6.6 KiB
Python
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() |