Versão Inicial V18 (FFmpeg + Haswell)
This commit is contained in:
264
app/modules/encoder.py
Executable file
264
app/modules/encoder.py
Executable file
@@ -0,0 +1,264 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user