inicio
This commit is contained in:
243
app/core/watcher.py
Normal file
243
app/core/watcher.py
Normal file
@@ -0,0 +1,243 @@
|
||||
import asyncio
|
||||
import os
|
||||
import shutil
|
||||
import re
|
||||
from pathlib import Path
|
||||
from database import AppConfig, Category
|
||||
from core.renamer import RenamerCore
|
||||
from core.ffmpeg_engine import FFmpegEngine
|
||||
from core.bot import TelegramManager
|
||||
from core.state import state
|
||||
|
||||
VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.mov', '.wmv'}
|
||||
|
||||
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):
|
||||
state.log("🛑 Solicitando Cancelamento...")
|
||||
self.abort_flag = True
|
||||
if self.current_process:
|
||||
try: self.current_process.kill()
|
||||
except: pass
|
||||
if self.pending_future and not self.pending_future.done():
|
||||
self.pending_future.cancel()
|
||||
|
||||
async def scan_folder(self, input_dir: Path):
|
||||
for file_path in input_dir.glob('**/*'):
|
||||
if self.abort_flag:
|
||||
self.abort_flag = False
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
async def process_pipeline(self, filepath: Path):
|
||||
fname = filepath.name
|
||||
self.abort_flag = False
|
||||
state.current_file = fname
|
||||
|
||||
state.update_task(fname, 'running', 0, 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:
|
||||
result['status'] = 'AMBIGUOUS'
|
||||
if 'match' in result: result['candidates'] = [result['match']]
|
||||
|
||||
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']}")
|
||||
|
||||
elif result['status'] == 'AMBIGUOUS':
|
||||
state.update_task(fname, 'warning', 10, label="Aguardando Telegram...")
|
||||
self.pending_future = asyncio.ensure_future(
|
||||
self.bot.ask_user_choice(fname, result['candidates'])
|
||||
)
|
||||
try:
|
||||
user_choice = await self.pending_future
|
||||
except asyncio.CancelledError:
|
||||
state.update_task(fname, 'error', 0, label="Cancelado Manualmente")
|
||||
return
|
||||
|
||||
self.pending_future = None
|
||||
if not user_choice or self.abort_flag:
|
||||
state.update_task(fname, 'skipped', 0, label="Ignorado/Cancelado")
|
||||
return
|
||||
|
||||
target_info = user_choice
|
||||
else:
|
||||
state.update_task(fname, 'error', 0, label="Não Identificado")
|
||||
state.log(f"❌ Falha TMDb: {result.get('msg', 'Desconhecido')}")
|
||||
return
|
||||
|
||||
if self.abort_flag: return
|
||||
|
||||
# 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
|
||||
try:
|
||||
# Recupera dados completos para montar o nome
|
||||
details = self.renamer.get_details(target_info['tmdb_id'], target_info['type'])
|
||||
|
||||
full_details = {
|
||||
'title': getattr(details, 'title', getattr(details, 'name', 'Unknown')),
|
||||
'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]
|
||||
|
||||
# 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']}")
|
||||
|
||||
engine = FFmpegEngine()
|
||||
total_duration = engine.get_duration(str(filepath))
|
||||
cmd = engine.build_command(str(filepath), str(temp_output))
|
||||
|
||||
self.current_process = await asyncio.create_subprocess_exec(
|
||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||
)
|
||||
|
||||
while True:
|
||||
if self.abort_flag:
|
||||
self.current_process.kill()
|
||||
break
|
||||
line_bytes = await self.current_process.stderr.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)
|
||||
|
||||
await self.current_process.wait()
|
||||
|
||||
if self.abort_flag:
|
||||
state.update_task(fname, 'error', 0, label="Abortado")
|
||||
if temp_output.exists(): os.remove(str(temp_output))
|
||||
return
|
||||
|
||||
if self.current_process.returncode != 0:
|
||||
state.update_task(fname, 'error', 0, label="Erro FFmpeg")
|
||||
return
|
||||
self.current_process = None
|
||||
|
||||
# 4. DEPLOY FINAL
|
||||
state.update_task(fname, 'running', 98, label="Organizando...")
|
||||
|
||||
final_full_path = Path(category.target_path) / relative_path
|
||||
final_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
shutil.move(str(temp_output), str(final_full_path))
|
||||
|
||||
if AppConfig.get_val('deploy_mode', 'move') == 'move':
|
||||
os.remove(str(filepath))
|
||||
|
||||
await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`")
|
||||
state.update_task(fname, 'done', 100, label=f"{full_details['title']}")
|
||||
state.current_file = ""
|
||||
|
||||
# Limpeza pasta vazia
|
||||
if AppConfig.get_val('cleanup_empty_folders', 'true') == 'true':
|
||||
try:
|
||||
parent = filepath.parent
|
||||
monitor_root = Path(AppConfig.get_val('monitor_path', '/downloads'))
|
||||
if parent != monitor_root and not any(parent.iterdir()):
|
||||
parent.rmdir()
|
||||
except: pass
|
||||
|
||||
except Exception as e:
|
||||
state.log(f"Erro Pipeline: {e}")
|
||||
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
|
||||
Reference in New Issue
Block a user