import streamlit as st import os import subprocess import shutil import json import re import time import threading from datetime import datetime # --- CONFIGURAÇÕES --- ROOT_DIR = "/downloads" OUTPUT_BASE = "/downloads/finalizados" STATUS_FILE = "/app/data/encoder_status.json" # --- GERENCIAMENTO DE ESTADO EM DISCO --- def save_status(data): """Salva o estado atual no disco para sobreviver ao F5""" try: with open(STATUS_FILE, 'w') as f: json.dump(data, f) except: pass def load_status(): """Lê o estado do disco""" if not os.path.exists(STATUS_FILE): return None try: with open(STATUS_FILE, 'r') as f: return json.load(f) except: return None def reset_status(): """Reseta o status para 'parado'""" save_status({ "is_running": False, "current_file": "", "progress": 0, "total_progress": 0, "log": "Aguardando...", "stop_requested": False }) # --- WORKER EM BACKGROUD --- class BackgroundWorker(threading.Thread): def __init__(self, input_folder, delete_orig): super().__init__() self.input_folder = input_folder self.delete_orig = delete_orig self.daemon = True # Daemon threads rodam em background independente da sessão def run(self): # 1. Preparação prepare_driver_environment() files_to_process = [] for r, d, f in os.walk(self.input_folder): if "/finalizados" in r or "/temp" in r: continue for file in f: if file.lower().endswith(('.mkv', '.mp4', '.avi')): files_to_process.append(os.path.join(r, file)) total = len(files_to_process) # Salva estado inicial state = { "is_running": True, "current_file": "Iniciando...", "progress": 0, "total_progress": 0, "log": "Preparando fila...", "stop_requested": False } save_status(state) # 2. Loop de Arquivos for i, fpath in enumerate(files_to_process): # Verifica se pediram pra parar current_state = load_status() if current_state and current_state.get("stop_requested"): break fname = os.path.basename(fpath) duration = get_video_duration(fpath) # Atualiza estado para arquivo atual state["current_file"] = fname state["progress"] = 0 state["total_progress"] = int((i / total) * 100) state["log"] = "Convertendo..." save_status(state) # Caminhos rel_path = os.path.relpath(fpath, self.input_folder) folder_name = os.path.basename(self.input_folder) if self.input_folder == ROOT_DIR: out_full_path = os.path.join(OUTPUT_BASE, rel_path) else: out_full_path = os.path.join(OUTPUT_BASE, folder_name, rel_path) os.makedirs(os.path.dirname(out_full_path), exist_ok=True) # Comando FFmpeg cmd = [ "ffmpeg", "-y", "-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128", "-hwaccel_output_format", "vaapi", "-i", fpath ] cmd += get_streams_map(fpath) cmd += [ "-c:v", "h264_vaapi", "-b:v", "4500k", "-compression_level", "1", "-c:a", "copy", "-c:s", "copy", out_full_path ] try: # Executa process = subprocess.Popen( cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1, universal_newlines=True, env=os.environ ) for line in process.stdout: # Checa stop a cada linha lida (para resposta rápida) chk_state = load_status() if chk_state and chk_state.get("stop_requested"): process.terminate() break if "time=" in line and duration: match = re.search(r"time=(\d{2}:\d{2}:\d{2}\.\d{2})", line) if match: secs = parse_time_to_seconds(match.group(1)) pct = int((secs / duration) * 100) if pct > 100: pct = 100 # Atualiza status no disco state["progress"] = pct # Pega velocidade speed_match = re.search(r"speed=\s*(\S+)", line) if speed_match: state["log"] = f"Velocidade: {speed_match.group(1)}" save_status(state) process.wait() if process.returncode == 0: if self.delete_orig: os.remove(fpath) except Exception as e: state["log"] = f"Erro: {str(e)}" save_status(state) # Fim reset_status() # --- FUNÇÕES AUXILIARES --- def prepare_driver_environment(): os.environ["LIBVA_DRIVER_NAME"] = "i965" drivers_ruins = ["/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so", "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so.1"] for driver in drivers_ruins: if os.path.exists(driver): try: os.remove(driver) except: pass def get_all_subfolders(root_path): folder_list = [root_path] try: for root, dirs, files in os.walk(root_path): if "finalizados" in root or "temp" in root: continue for d in dirs: folder_list.append(os.path.join(root, d)) except: pass return sorted(folder_list) def get_video_duration(filepath): cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filepath] try: return float(subprocess.check_output(cmd).decode().strip()) except: return None def parse_time_to_seconds(time_str): h, m, s = time_str.split(':') return int(h) * 3600 + int(m) * 60 + float(s) def get_streams_map(filepath): cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", filepath] try: res = subprocess.run(cmd, capture_output=True, text=True) data = json.loads(res.stdout) except: return ["-map", "0"] map_args = ["-map", "0:v"] audio_found = False for s in data.get('streams', []): if s['codec_type'] == 'audio': lang = s.get('tags', {}).get('language', 'und').lower() if lang in ['por', 'pt', 'eng', 'en', 'jpn', 'ja', 'und']: map_args.extend(["-map", f"0:{s['index']}"]) audio_found = True if not audio_found: map_args.extend(["-map", "0:a"]) for s in data.get('streams', []): if s['codec_type'] == 'subtitle': lang = s.get('tags', {}).get('language', 'und').lower() if lang in ['por', 'pt', 'pob', 'pt-br']: map_args.extend(["-map", f"0:{s['index']}"]) return map_args # --- RENDERIZAÇÃO --- def render(): st.header("⚙️ Encoder (Persistente)") st.info("O processo continua rodando mesmo se você fechar a página.") # 1. Lê estado do disco status = load_status() # Se não existe ou não está rodando if not status or not status.get("is_running"): all_folders = get_all_subfolders(ROOT_DIR) idx = 0 if 'enc_last_folder' in st.session_state and st.session_state.enc_last_folder in all_folders: idx = all_folders.index(st.session_state.enc_last_folder) input_folder = st.selectbox("Selecione a pasta:", options=all_folders, index=idx, format_func=lambda x: x.replace(ROOT_DIR, "") if x != ROOT_DIR else "Raiz") st.session_state.enc_last_folder = input_folder delete_orig = st.checkbox("🗑️ Excluir originais após sucesso?", value=False) if st.button("🚀 Iniciar Processo", type="primary"): if not os.path.exists(input_folder): st.error("Pasta inválida") else: # Inicia Thread t = BackgroundWorker(input_folder, delete_orig) t.start() time.sleep(1) # Dá tempo de criar o arquivo json st.rerun() else: # MODO MONITORAMENTO st.success("🔄 Sistema Rodando...") col1, col2 = st.columns([3, 1]) with col1: st.write(f"📁 **Processando:** `{status.get('current_file')}`") st.progress(status.get('progress', 0), text=f"Arquivo: {status.get('progress')}%") st.progress(status.get('total_progress', 0), text=f"Total: {status.get('total_progress')}%") st.caption(status.get('log')) with col2: if st.button("🛑 Parar", type="secondary"): status["stop_requested"] = True save_status(status) st.warning("Parando...") if st.button("🔄 Refresh"): st.rerun() # Atualiza a tela a cada 5s time.sleep(5) st.rerun()