From 15eb14bf28aff01fafe58a5037b420853a56e3eb Mon Sep 17 00:00:00 2001 From: Creidsu Date: Wed, 11 Feb 2026 23:18:13 +0000 Subject: [PATCH] melhorado e compartibilidade de x265 decoder cpu e encoder x264 --- app/core/ffmpeg_engine.py | 74 +++++++++++++++++-------------- app/core/watcher.py | 91 ++++++++++++++++++++++++--------------- 2 files changed, 98 insertions(+), 67 deletions(-) diff --git a/app/core/ffmpeg_engine.py b/app/core/ffmpeg_engine.py index 09ba015..2575ebe 100755 --- a/app/core/ffmpeg_engine.py +++ b/app/core/ffmpeg_engine.py @@ -15,6 +15,7 @@ class FFmpegEngine: state.log("⚠️ AVISO: Nenhum perfil FFmpeg ativo! Usando defaults.") def get_file_info(self, filepath): + """Extrai metadados do arquivo usando ffprobe""" cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', filepath] try: output = subprocess.check_output(cmd).decode('utf-8') @@ -33,56 +34,68 @@ class FFmpegEngine: metadata = self.get_file_info(input_file) if not metadata: raise Exception("Arquivo inválido.") - cmd = ['ffmpeg', '-y'] - - # --- CONFIGURAÇÃO DE HARDWARE OTIMIZADA --- - video_filters = [] + # Descobre o codec de vídeo de entrada + video_stream = next((s for s in metadata['streams'] if s['codec_type'] == 'video'), None) + input_codec = video_stream['codec_name'] if video_stream else 'unknown' + + # --- DETECÇÃO DE HARDWARE (Haswell Safe Logic) --- + # Intel 4ª Geração (Haswell): + # Decode: Suporta H264, MPEG2. NÃO SUPORTA HEVC/x265. + + can_hw_decode = False + hw_decodable_codecs = ['h264', 'mpeg2video', 'vc1', 'mjpeg'] if 'vaapi' in p.video_codec: - # TENTATIVA OTIMIZADA: HWAccel habilitado na entrada - # Isso reduz a CPU drasticamente se o decode for suportado - cmd.extend(['-hwaccel', 'vaapi']) - cmd.extend(['-hwaccel_device', '/dev/dri/renderD128']) - cmd.extend(['-hwaccel_output_format', 'vaapi']) - - # Não precisamos de 'hwupload' se o output format já é vaapi - # Mas vamos deixar o filtro scale_vaapi caso precise redimensionar (opcional) - # video_filters.append('scale_vaapi=format=nv12') + if input_codec in hw_decodable_codecs: + can_hw_decode = True + else: + state.log(f"⚙️ Modo Híbrido (Haswell): CPU lendo {input_codec} -> GPU codificando.") + + cmd = ['ffmpeg', '-y'] + + # --- INPUT (LEITURA) --- + if 'vaapi' in p.video_codec: + # Inicializa o dispositivo VAAPI + cmd.extend(['-init_hw_device', 'vaapi=intel:/dev/dri/renderD128']) + cmd.extend(['-filter_hw_device', 'intel']) + if can_hw_decode: + # Se a GPU sabe ler, usa aceleração total + cmd.extend(['-hwaccel', 'vaapi']) + cmd.extend(['-hwaccel_output_format', 'vaapi']) + cmd.extend(['-hwaccel_device', 'intel']) + elif 'qsv' in p.video_codec: - cmd.extend(['-hwaccel', 'qsv', '-c:v', 'h264_qsv']) - + cmd.extend(['-hwaccel', 'qsv', '-c:v', 'h264_qsv']) elif 'nvenc' in p.video_codec: cmd.extend(['-hwaccel', 'cuda']) - # Input e Threads (Limita CPU se cair pra software decode) cmd.extend(['-threads', '4', '-i', input_file]) - # --- VÍDEO --- + # --- PROCESSAMENTO E SAÍDA --- cmd.extend(['-map', '0:v:0']) + video_filters = [] if p.video_codec == 'copy': cmd.extend(['-c:v', 'copy']) else: - cmd.extend(['-c:v', p.video_codec]) - - # Filtros + # Se for VAAPI Híbrido (leitura CPU), precisamos subir pra GPU + if 'vaapi' in p.video_codec and not can_hw_decode: + video_filters.append('format=nv12,hwupload') + if video_filters: cmd.extend(['-vf', ",".join(video_filters)]) - - # Qualidade - if 'vaapi' in p.video_codec: - # -qp é o modo Qualidade Constante do VAAPI - # Se der erro, remova -qp e use -b:v 5M - cmd.extend(['-qp', str(p.crf)]) + cmd.extend(['-c:v', p.video_codec]) + + if 'vaapi' in p.video_codec: + cmd.extend(['-qp', str(p.crf)]) elif 'nvenc' in p.video_codec: cmd.extend(['-cq', str(p.crf), '-preset', p.preset]) - elif 'libx264' in p.video_codec: cmd.extend(['-crf', str(p.crf), '-preset', p.preset]) - # --- ÁUDIO (AAC Stereo) --- + # --- ÁUDIO (AAC) --- allowed_langs = [l.strip().lower() for l in (p.audio_langs or "").split(',')] audio_streams = [s for s in metadata['streams'] if s['codec_type'] == 'audio'] @@ -91,14 +104,13 @@ class FFmpegEngine: lang = s.get('tags', {}).get('language', 'und').lower() if not allowed_langs or lang in allowed_langs or 'und' in allowed_langs: cmd.extend(['-map', f'0:{s["index"]}']) - # AAC é leve e compatível cmd.extend([f'-c:a:{acount}', 'aac', f'-b:a:{acount}', '192k', f'-ac:{acount}', '2']) acount += 1 if acount == 0 and audio_streams: cmd.extend(['-map', '0:a:0', '-c:a', 'aac', '-b:a', '192k']) - # --- LEGENDAS (Copy) --- + # --- LEGENDAS --- sub_allowed = [l.strip().lower() for l in (p.subtitle_langs or "").split(',')] sub_streams = [s for s in metadata['streams'] if s['codec_type'] == 'subtitle'] @@ -110,10 +122,8 @@ class FFmpegEngine: cmd.extend([f'-c:s:{scount}', 'copy']) scount += 1 - # Metadados clean_title = os.path.splitext(os.path.basename(output_file))[0] cmd.extend(['-metadata', f'title={clean_title}']) cmd.append(output_file) - state.log(f"🛠️ CMD: {' '.join(cmd)}") return cmd \ No newline at end of file diff --git a/app/core/watcher.py b/app/core/watcher.py index 82c726d..a283f6a 100644 --- a/app/core/watcher.py +++ b/app/core/watcher.py @@ -119,33 +119,28 @@ class DirectoryWatcher: if self.abort_flag: return - # --- METADADOS E CATEGORIA --- + # --- METADADOS --- try: details = self.renamer.get_details(target_info['tmdb_id'], target_info['type']) r_title = getattr(details, 'title', getattr(details, 'name', 'Unknown')) or 'Unknown' - # Extração de IDs e Códigos para a nova Lógica - # IDs de Gênero (Ex: [16, 28]) tmdb_genre_ids = [str(g['id']) for g in getattr(details, 'genres', [])] - # Países de Origem (Ex: ['JP', 'US']) origin_countries = getattr(details, 'origin_country', []) if isinstance(origin_countries, str): origin_countries = [origin_countries] origin_countries = [c.upper() for c in origin_countries] - state.log(f"ℹ️ Dados: Gêneros={tmdb_genre_ids} | Países={origin_countries}") - 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'] = str(d_date)[:4] - # --- 2. CATEGORIA INTELIGENTE (NOVA LÓGICA) --- + # --- 2. CATEGORIA INTELIGENTE --- category = self.find_best_category(target_info['type'], tmdb_genre_ids, origin_countries) if not category: state.update_task(fname, 'error', 0, label="Sem Categoria") return - state.log(f"📂 Categoria Vencedora: {category.name}") + state.log(f"📂 Categoria: {category.name}") # --- 3. CONVERSÃO --- guessed_data = result.get('guessed', {}) @@ -157,27 +152,59 @@ class DirectoryWatcher: state.update_task(fname, 'running', progress=15, label=f"Convertendo: {full_details['title']}") engine = FFmpegEngine() + + # Pega duração (Segundos) total_duration_sec = engine.get_duration(str(filepath)) total_duration_us = total_duration_sec * 1000000 + if total_duration_sec == 0: + state.log("⚠️ Aviso: Não foi possível ler a duração do vídeo. Progresso pode falhar.") + cmd = engine.build_command(str(filepath), str(temp_output)) + # Injeta flags para progresso via Pipe cmd.insert(1, '-progress'); cmd.insert(2, 'pipe:1') + # Log do comando para debug + printable_cmd = " ".join([f'"{c}"' if " " in c else c for c in cmd]) + state.log(f"🛠️ Executando FFmpeg (Debug no Log): ...{printable_cmd[-50:]}") # Log curto + self.current_process = await asyncio.create_subprocess_exec( *cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE ) current_speed_str = "" + + # Loop de Leitura (CORRIGIDO E COMPLETO) while True: if self.abort_flag: self.current_process.kill() break + + # Lê stdout (Progresso) line_bytes = await self.current_process.stdout.readline() - if not line_bytes: break + + if not line_bytes: + # Se stdout acabou, verifica se o processo morreu + if self.current_process.returncode is not None: + break + + # Verifica stderr se houver erro + err_bytes = await self.current_process.stderr.read(1024) + if err_bytes: + err_msg = err_bytes.decode('utf-8', errors='ignore') + # Loga apenas erros reais, ignora warnings comuns + if "Error" in err_msg or "Invalid" in err_msg: + state.log(f"FFmpeg Stderr: {err_msg}") + + if not line_bytes and not err_bytes: + break # Acabou tudo + line = line_bytes.decode('utf-8', errors='ignore').strip() + if '=' in line: key, value = line.split('=', 1) key = key.strip(); value = value.strip() + if key == 'out_time_us': try: current_us = int(value) @@ -185,10 +212,20 @@ class DirectoryWatcher: file_pct = (current_us / total_duration_us) * 100 if file_pct > 100: file_pct = 100 if file_pct < 0: file_pct = 0 + 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']}") + + state.update_task( + fname, 'running', + progress=global_pct, + file_progress=file_pct, + speed=current_speed_str, + label=f"Processando: {full_details['title']}" + ) except: pass - elif key == 'speed': current_speed_str = value + + elif key == 'speed': + current_speed_str = value await self.current_process.wait() @@ -198,8 +235,10 @@ class DirectoryWatcher: return if self.current_process.returncode != 0: + # Se falhar, tenta ler o resto do stderr err_log = await self.current_process.stderr.read() - state.log(f"❌ Erro FFmpeg: {err_log.decode('utf-8')[-200:]}") + msg = err_log.decode('utf-8')[-300:] + state.log(f"❌ Erro FFmpeg Fatal: {msg}") state.update_task(fname, 'error', 0, label="Erro FFmpeg") return self.current_process = None @@ -238,14 +277,11 @@ class DirectoryWatcher: def find_best_category(self, media_type, genre_ids, countries): """ Sistema de Pontuação 3.0 (Regras Estritas) - Agora compara IDs e Siglas, não texto. """ all_cats = list(Category.select()) if not all_cats: return None candidates = [] - - # 1. Filtro Rígido de TIPO (Movie vs Series) for cat in all_cats: if media_type == 'movie' and cat.content_type == 'series': continue if media_type != 'movie' and cat.content_type == 'movie': continue @@ -257,53 +293,38 @@ class DirectoryWatcher: for cat in candidates: score = 0 - # Carrega filtros da categoria cat_genres = cat.genre_filters.split(',') if cat.genre_filters else [] cat_countries = cat.country_filters.split(',') if cat.country_filters else [] - cat_genres = [g for g in cat_genres if g] # Limpa vazios + cat_genres = [g for g in cat_genres if g] cat_countries = [c for c in cat_countries if c] - # --- Lógica de Correspondência --- match_genre = False match_country = False - # Verifica Gêneros (Se a categoria tiver filtros, TEM que bater) if cat_genres: - # Se o arquivo tiver pelo menos UM dos gêneros da lista if any(gid in cat_genres for gid in genre_ids): - match_genre = True - score += 50 # Ganha muitos pontos por match específico + score += 50 else: - # Se a categoria exige gênero e o arquivo não tem -> Categoria Descartada score = -1000 else: - # Categoria genérica de gênero (aceita tudo) - match_genre = True - score += 10 # Pontuação base baixa + score += 10 - # Verifica Países if cat_countries: if any(cc in cat_countries for cc in countries): - match_country = True - score += 50 # Ganha pontos por match de país + score += 50 else: - score = -1000 # Exige país mas não bate -> Descartada + score = -1000 else: - match_country = True score += 10 - # Bônus se for categoria Mista (geralmente usada para Anime/Desenho) if cat.content_type == 'mixed' and score > 0: score += 5 scored_cats.append((score, cat)) - # Ordena scored_cats.sort(key=lambda x: x[0], reverse=True) best_match = scored_cats[0] - # Se a pontuação for negativa, significa que nenhuma regra bateu. - # Devemos pegar uma categoria genérica (sem filtros) if best_match[0] < 0: for cat in candidates: if not cat.genre_filters and not cat.country_filters: