corrigido o progresso na inteface e o encoder

This commit is contained in:
2026-02-09 00:37:57 +00:00
parent e273233e38
commit 3fe63c0bc6
9 changed files with 1684 additions and 160 deletions

View File

@@ -15,40 +15,29 @@ class DirectoryWatcher:
def __init__(self, bot: TelegramManager):
self.bot = bot
self.renamer = RenamerCore()
# Inicia pausado (True só quando ativado no Dashboard)
self.is_running = False
self.temp_dir = Path('/app/temp')
self.temp_dir.mkdir(parents=True, exist_ok=True)
self.current_watch_path = None
# Controle de Processo
self.current_process = None
self.pending_future = None
self.abort_flag = False
state.watcher = self
async def start(self):
"""Inicia o loop do serviço"""
state.log("🟡 Watcher Service: Pronto. Aguardando ativação no Dashboard...")
while True:
if self.is_running:
try:
config_path = AppConfig.get_val('monitor_path', '/downloads')
watch_dir = Path(config_path)
if str(watch_dir) != str(self.current_watch_path):
state.log(f"📁 Monitorando: {watch_dir}")
self.current_watch_path = watch_dir
if watch_dir.exists():
await self.scan_folder(watch_dir)
except Exception as e:
state.log(f"❌ Erro Watcher Loop: {e}")
await asyncio.sleep(5)
def abort_current_task(self):
@@ -67,23 +56,16 @@ class DirectoryWatcher:
if state.current_file:
state.update_task(state.current_file, 'error', label=f"{state.current_file} (Cancelado)")
return
if not self.is_running: return
if file_path.is_file() and file_path.suffix.lower() in VIDEO_EXTENSIONS:
if file_path.name.startswith('.') or 'processing' in file_path.name: continue
# Ignora se já terminou nesta sessão
if file_path.name in state.tasks and state.tasks[file_path.name]['status'] == 'done':
continue
if file_path.name in state.tasks and state.tasks[file_path.name]['status'] == 'done': continue
try:
s1 = file_path.stat().st_size
await asyncio.sleep(1)
s2 = file_path.stat().st_size
if s1 != s2: continue
except: continue
await self.process_pipeline(file_path)
if self.abort_flag: return
@@ -92,23 +74,28 @@ class DirectoryWatcher:
self.abort_flag = False
state.current_file = fname
state.update_task(fname, 'running', 0, label=f"Identificando: {fname}...")
# Etapa 1: Identificação (Global 0-15%)
state.update_task(fname, 'running', progress=5, label=f"Identificando: {fname}...")
state.log(f"🔄 Iniciando: {fname}")
# 1. IDENTIFICAÇÃO
result = self.renamer.identify_file(str(filepath))
target_info = None
is_semi_auto = AppConfig.get_val('semi_auto', 'false') == 'true'
if is_semi_auto:
if is_semi_auto and result['status'] == 'MATCH':
result['status'] = 'AMBIGUOUS'
if 'match' in result: result['candidates'] = [result['match']]
result['candidates'] = [result['match']]
target_info = None
if result['status'] == 'MATCH':
target_info = {'tmdb_id': result['match']['tmdb_id'], 'type': result['match']['type']}
state.update_task(fname, 'running', 10, label=f"ID: {result['match']['title']}")
state.update_task(fname, 'running', progress=10, label=f"ID: {result['match']['title']}")
elif result['status'] == 'AMBIGUOUS':
if 'candidates' not in result or not result['candidates']:
state.update_task(fname, 'error', 0, label="Erro: Ambíguo sem candidatos")
return
state.update_task(fname, 'warning', 10, label="Aguardando Telegram...")
self.pending_future = asyncio.ensure_future(
self.bot.ask_user_choice(fname, result['candidates'])
@@ -123,67 +110,104 @@ class DirectoryWatcher:
if not user_choice or self.abort_flag:
state.update_task(fname, 'skipped', 0, label="Ignorado/Cancelado")
return
target_info = user_choice
else:
msg = result.get('msg', 'Desconhecido')
state.update_task(fname, 'error', 0, label="Não Identificado")
state.log(f"❌ Falha TMDb: {result.get('msg', 'Desconhecido')}")
state.log(f"❌ Falha Identificação: {msg}")
return
if self.abort_flag: return
# 2. CATEGORIA
# Etapa 2: Categoria
category = self.find_category(target_info['type'])
if not category:
state.update_task(fname, 'error', 0, label="Sem Categoria")
return
# 3. CONVERSÃO & CAMINHO
# Etapa 3: Conversão
try:
# Recupera dados completos para montar o nome
details = self.renamer.get_details(target_info['tmdb_id'], target_info['type'])
r_title = getattr(details, 'title', getattr(details, 'name', 'Unknown')) or 'Unknown'
full_details = {
'title': getattr(details, 'title', getattr(details, 'name', 'Unknown')),
'year': '0000',
'type': target_info['type']
}
full_details = {'title': r_title, 'year': '0000', 'type': target_info['type']}
d_date = getattr(details, 'release_date', getattr(details, 'first_air_date', '0000'))
if d_date: full_details['year'] = d_date[:4]
if d_date: full_details['year'] = str(d_date)[:4]
# Gera caminho inteligente
guessed_data = result.get('guessed', {})
relative_path = self.renamer.build_path(category, full_details, guessed_data)
temp_filename = os.path.basename(relative_path)
temp_output = self.temp_dir / temp_filename
state.update_task(fname, 'running', 15, label=f"Convertendo: {full_details['title']}")
state.update_task(fname, 'running', progress=15, label=f"Convertendo: {full_details['title']}")
engine = FFmpegEngine()
total_duration = engine.get_duration(str(filepath))
# Tenta pegar duração total em Segundos
total_duration_sec = engine.get_duration(str(filepath))
# Converte para microsegundos para bater com o FFmpeg
total_duration_us = total_duration_sec * 1000000
cmd = engine.build_command(str(filepath), str(temp_output))
# --- MODIFICAÇÃO: Injeta flag para saída legível por máquina (stdout) ---
# Isso garante que teremos dados de progresso confiáveis
cmd.insert(1, '-progress')
cmd.insert(2, 'pipe:1')
# Redireciona stdout para pegarmos os dados. Stderr fica para erros/warnings.
self.current_process = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
)
# --- LOOP DE PROGRESSO (MÉTODO KEY=VALUE) ---
current_speed_str = ""
while True:
if self.abort_flag:
self.current_process.kill()
break
line_bytes = await self.current_process.stderr.readline()
# Lê stdout (progresso)
line_bytes = await self.current_process.stdout.readline()
if not line_bytes: break
line = line_bytes.decode('utf-8', errors='ignore')
time_match = re.search(r'time=(\d{2}):(\d{2}):(\d{2})', line)
if time_match and total_duration > 0:
h, m, s = map(int, time_match.groups())
current_seconds = h*3600 + m*60 + s
pct = 15 + ((current_seconds / total_duration) * 80)
state.update_task(fname, 'running', pct)
line = line_bytes.decode('utf-8', errors='ignore').strip()
# O formato é chave=valor
if '=' in line:
key, value = line.split('=', 1)
key = key.strip()
value = value.strip()
# 1. Tempo decorrido (em microsegundos)
if key == 'out_time_us':
try:
current_us = int(value)
if total_duration_us > 0:
file_pct = (current_us / total_duration_us) * 100
if file_pct > 100: file_pct = 100
if file_pct < 0: file_pct = 0 # As vezes vem negativo no inicio
# Global: 15% a 99%
global_pct = 15 + (file_pct * 0.84)
state.update_task(
fname, 'running',
progress=global_pct,
file_progress=file_pct,
speed=current_speed_str,
label=f"Processando: {full_details['title']}"
)
except: pass
# 2. Velocidade
elif key == 'speed':
current_speed_str = value
await self.current_process.wait()
# -----------------------------------
if self.abort_flag:
state.update_task(fname, 'error', 0, label="Abortado")
@@ -191,36 +215,32 @@ class DirectoryWatcher:
return
if self.current_process.returncode != 0:
# Se falhar, lemos o stderr para saber o motivo
err_log = await self.current_process.stderr.read()
state.log(f"❌ Erro FFmpeg: {err_log.decode('utf-8')[-200:]}")
state.update_task(fname, 'error', 0, label="Erro FFmpeg")
return
self.current_process = None
# 4. DEPLOY SEGURO
state.update_task(fname, 'running', 98, label="Organizando...")
# Monta caminho completo
# Etapa 4: Deploy
state.update_task(fname, 'running', progress=99, label="Organizando...")
final_full_path = Path(category.target_path) / relative_path
# Garante que a PASTA existe (Merge seguro)
# Se a pasta já existe, o mkdir(exist_ok=True) não faz nada (não apaga, não mexe)
final_full_path.parent.mkdir(parents=True, exist_ok=True)
# Tratamento de ARQUIVO duplicado
if final_full_path.exists():
# Se o arquivo já existe, apagamos ele para substituir pelo novo (Upgrade)
state.log(f"⚠️ Substituindo arquivo existente: {final_full_path.name}")
os.remove(str(final_full_path))
try: os.remove(str(final_full_path))
except: pass
# Move APENAS o arquivo
shutil.move(str(temp_output), str(final_full_path))
if AppConfig.get_val('deploy_mode', 'move') == 'move':
os.remove(str(filepath))
try: os.remove(str(filepath))
except: pass
await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`")
state.update_task(fname, 'done', 100, label=f"{full_details['title']}")
await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`\n📂 {category.name}")
state.update_task(fname, 'done', 100, label=f"{full_details['title']}", file_progress=100, speed="Finalizado")
state.current_file = ""
# Limpeza pasta vazia
if AppConfig.get_val('cleanup_empty_folders', 'true') == 'true':
try:
parent = filepath.parent
@@ -234,20 +254,11 @@ class DirectoryWatcher:
state.update_task(fname, 'error', 0, label=f"Erro: {e}")
def find_category(self, media_type):
"""Encontra a categoria correta baseada nas keywords"""
# Define keywords esperadas
keywords = ['movie', 'film', 'filme'] if media_type == 'movie' else ['tv', 'serie', 'série']
all_cats = list(Category.select())
for cat in all_cats:
if not cat.match_keywords: continue
# --- AQUI ESTAVA O ERRO POTENCIAL ---
# O .strip() precisa dos parênteses antes do .lower()
cat_keys = [k.strip().lower() for k in cat.match_keywords.split(',')]
if any(k in cat_keys for k in keywords):
return cat
# Fallback (primeira categoria que existir)
return all_cats[0] if all_cats else None