Files
pymediamanager/app/modules/downloader.py

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()