melhorado e compartibilidade de x265 decoder cpu e encoder x264
This commit is contained in:
@@ -15,6 +15,7 @@ class FFmpegEngine:
|
|||||||
state.log("⚠️ AVISO: Nenhum perfil FFmpeg ativo! Usando defaults.")
|
state.log("⚠️ AVISO: Nenhum perfil FFmpeg ativo! Usando defaults.")
|
||||||
|
|
||||||
def get_file_info(self, filepath):
|
def get_file_info(self, filepath):
|
||||||
|
"""Extrai metadados do arquivo usando ffprobe"""
|
||||||
cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', filepath]
|
cmd = ['ffprobe', '-v', 'quiet', '-print_format', 'json', '-show_streams', '-show_format', filepath]
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(cmd).decode('utf-8')
|
output = subprocess.check_output(cmd).decode('utf-8')
|
||||||
@@ -33,56 +34,68 @@ class FFmpegEngine:
|
|||||||
metadata = self.get_file_info(input_file)
|
metadata = self.get_file_info(input_file)
|
||||||
if not metadata: raise Exception("Arquivo inválido.")
|
if not metadata: raise Exception("Arquivo inválido.")
|
||||||
|
|
||||||
cmd = ['ffmpeg', '-y']
|
# 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'
|
||||||
|
|
||||||
# --- CONFIGURAÇÃO DE HARDWARE OTIMIZADA ---
|
# --- DETECÇÃO DE HARDWARE (Haswell Safe Logic) ---
|
||||||
video_filters = []
|
# 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:
|
if 'vaapi' in p.video_codec:
|
||||||
# TENTATIVA OTIMIZADA: HWAccel habilitado na entrada
|
if input_codec in hw_decodable_codecs:
|
||||||
# Isso reduz a CPU drasticamente se o decode for suportado
|
can_hw_decode = True
|
||||||
cmd.extend(['-hwaccel', 'vaapi'])
|
else:
|
||||||
cmd.extend(['-hwaccel_device', '/dev/dri/renderD128'])
|
state.log(f"⚙️ Modo Híbrido (Haswell): CPU lendo {input_codec} -> GPU codificando.")
|
||||||
cmd.extend(['-hwaccel_output_format', 'vaapi'])
|
|
||||||
|
|
||||||
# Não precisamos de 'hwupload' se o output format já é vaapi
|
cmd = ['ffmpeg', '-y']
|
||||||
# Mas vamos deixar o filtro scale_vaapi caso precise redimensionar (opcional)
|
|
||||||
# video_filters.append('scale_vaapi=format=nv12')
|
# --- 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:
|
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:
|
elif 'nvenc' in p.video_codec:
|
||||||
cmd.extend(['-hwaccel', 'cuda'])
|
cmd.extend(['-hwaccel', 'cuda'])
|
||||||
|
|
||||||
# Input e Threads (Limita CPU se cair pra software decode)
|
|
||||||
cmd.extend(['-threads', '4', '-i', input_file])
|
cmd.extend(['-threads', '4', '-i', input_file])
|
||||||
|
|
||||||
# --- VÍDEO ---
|
# --- PROCESSAMENTO E SAÍDA ---
|
||||||
cmd.extend(['-map', '0:v:0'])
|
cmd.extend(['-map', '0:v:0'])
|
||||||
|
video_filters = []
|
||||||
|
|
||||||
if p.video_codec == 'copy':
|
if p.video_codec == 'copy':
|
||||||
cmd.extend(['-c:v', 'copy'])
|
cmd.extend(['-c:v', 'copy'])
|
||||||
else:
|
else:
|
||||||
cmd.extend(['-c:v', p.video_codec])
|
# 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')
|
||||||
|
|
||||||
# Filtros
|
|
||||||
if video_filters:
|
if video_filters:
|
||||||
cmd.extend(['-vf', ",".join(video_filters)])
|
cmd.extend(['-vf', ",".join(video_filters)])
|
||||||
|
|
||||||
# Qualidade
|
cmd.extend(['-c:v', p.video_codec])
|
||||||
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)])
|
|
||||||
|
|
||||||
|
if 'vaapi' in p.video_codec:
|
||||||
|
cmd.extend(['-qp', str(p.crf)])
|
||||||
elif 'nvenc' in p.video_codec:
|
elif 'nvenc' in p.video_codec:
|
||||||
cmd.extend(['-cq', str(p.crf), '-preset', p.preset])
|
cmd.extend(['-cq', str(p.crf), '-preset', p.preset])
|
||||||
|
|
||||||
elif 'libx264' in p.video_codec:
|
elif 'libx264' in p.video_codec:
|
||||||
cmd.extend(['-crf', str(p.crf), '-preset', p.preset])
|
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(',')]
|
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']
|
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()
|
lang = s.get('tags', {}).get('language', 'und').lower()
|
||||||
if not allowed_langs or lang in allowed_langs or 'und' in allowed_langs:
|
if not allowed_langs or lang in allowed_langs or 'und' in allowed_langs:
|
||||||
cmd.extend(['-map', f'0:{s["index"]}'])
|
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'])
|
cmd.extend([f'-c:a:{acount}', 'aac', f'-b:a:{acount}', '192k', f'-ac:{acount}', '2'])
|
||||||
acount += 1
|
acount += 1
|
||||||
|
|
||||||
if acount == 0 and audio_streams:
|
if acount == 0 and audio_streams:
|
||||||
cmd.extend(['-map', '0:a:0', '-c:a', 'aac', '-b:a', '192k'])
|
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_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']
|
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'])
|
cmd.extend([f'-c:s:{scount}', 'copy'])
|
||||||
scount += 1
|
scount += 1
|
||||||
|
|
||||||
# Metadados
|
|
||||||
clean_title = os.path.splitext(os.path.basename(output_file))[0]
|
clean_title = os.path.splitext(os.path.basename(output_file))[0]
|
||||||
cmd.extend(['-metadata', f'title={clean_title}'])
|
cmd.extend(['-metadata', f'title={clean_title}'])
|
||||||
cmd.append(output_file)
|
cmd.append(output_file)
|
||||||
|
|
||||||
state.log(f"🛠️ CMD: {' '.join(cmd)}")
|
|
||||||
return cmd
|
return cmd
|
||||||
@@ -119,33 +119,28 @@ class DirectoryWatcher:
|
|||||||
|
|
||||||
if self.abort_flag: return
|
if self.abort_flag: return
|
||||||
|
|
||||||
# --- METADADOS E CATEGORIA ---
|
# --- METADADOS ---
|
||||||
try:
|
try:
|
||||||
details = self.renamer.get_details(target_info['tmdb_id'], target_info['type'])
|
details = self.renamer.get_details(target_info['tmdb_id'], target_info['type'])
|
||||||
r_title = getattr(details, 'title', getattr(details, 'name', 'Unknown')) or 'Unknown'
|
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', [])]
|
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', [])
|
origin_countries = getattr(details, 'origin_country', [])
|
||||||
if isinstance(origin_countries, str): origin_countries = [origin_countries]
|
if isinstance(origin_countries, str): origin_countries = [origin_countries]
|
||||||
origin_countries = [c.upper() for c in 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']}
|
full_details = {'title': r_title, 'year': '0000', 'type': target_info['type']}
|
||||||
d_date = getattr(details, 'release_date', getattr(details, 'first_air_date', '0000'))
|
d_date = getattr(details, 'release_date', getattr(details, 'first_air_date', '0000'))
|
||||||
if d_date: full_details['year'] = str(d_date)[:4]
|
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)
|
category = self.find_best_category(target_info['type'], tmdb_genre_ids, origin_countries)
|
||||||
|
|
||||||
if not category:
|
if not category:
|
||||||
state.update_task(fname, 'error', 0, label="Sem Categoria")
|
state.update_task(fname, 'error', 0, label="Sem Categoria")
|
||||||
return
|
return
|
||||||
|
|
||||||
state.log(f"📂 Categoria Vencedora: {category.name}")
|
state.log(f"📂 Categoria: {category.name}")
|
||||||
|
|
||||||
# --- 3. CONVERSÃO ---
|
# --- 3. CONVERSÃO ---
|
||||||
guessed_data = result.get('guessed', {})
|
guessed_data = result.get('guessed', {})
|
||||||
@@ -157,27 +152,59 @@ class DirectoryWatcher:
|
|||||||
state.update_task(fname, 'running', progress=15, label=f"Convertendo: {full_details['title']}")
|
state.update_task(fname, 'running', progress=15, label=f"Convertendo: {full_details['title']}")
|
||||||
|
|
||||||
engine = FFmpegEngine()
|
engine = FFmpegEngine()
|
||||||
|
|
||||||
|
# Pega duração (Segundos)
|
||||||
total_duration_sec = engine.get_duration(str(filepath))
|
total_duration_sec = engine.get_duration(str(filepath))
|
||||||
total_duration_us = total_duration_sec * 1000000
|
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))
|
cmd = engine.build_command(str(filepath), str(temp_output))
|
||||||
|
# Injeta flags para progresso via Pipe
|
||||||
cmd.insert(1, '-progress'); cmd.insert(2, 'pipe:1')
|
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(
|
self.current_process = await asyncio.create_subprocess_exec(
|
||||||
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
*cmd, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE
|
||||||
)
|
)
|
||||||
|
|
||||||
current_speed_str = ""
|
current_speed_str = ""
|
||||||
|
|
||||||
|
# Loop de Leitura (CORRIGIDO E COMPLETO)
|
||||||
while True:
|
while True:
|
||||||
if self.abort_flag:
|
if self.abort_flag:
|
||||||
self.current_process.kill()
|
self.current_process.kill()
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Lê stdout (Progresso)
|
||||||
line_bytes = await self.current_process.stdout.readline()
|
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()
|
line = line_bytes.decode('utf-8', errors='ignore').strip()
|
||||||
|
|
||||||
if '=' in line:
|
if '=' in line:
|
||||||
key, value = line.split('=', 1)
|
key, value = line.split('=', 1)
|
||||||
key = key.strip(); value = value.strip()
|
key = key.strip(); value = value.strip()
|
||||||
|
|
||||||
if key == 'out_time_us':
|
if key == 'out_time_us':
|
||||||
try:
|
try:
|
||||||
current_us = int(value)
|
current_us = int(value)
|
||||||
@@ -185,10 +212,20 @@ class DirectoryWatcher:
|
|||||||
file_pct = (current_us / total_duration_us) * 100
|
file_pct = (current_us / total_duration_us) * 100
|
||||||
if file_pct > 100: file_pct = 100
|
if file_pct > 100: file_pct = 100
|
||||||
if file_pct < 0: file_pct = 0
|
if file_pct < 0: file_pct = 0
|
||||||
|
|
||||||
global_pct = 15 + (file_pct * 0.84)
|
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
|
except: pass
|
||||||
elif key == 'speed': current_speed_str = value
|
|
||||||
|
elif key == 'speed':
|
||||||
|
current_speed_str = value
|
||||||
|
|
||||||
await self.current_process.wait()
|
await self.current_process.wait()
|
||||||
|
|
||||||
@@ -198,8 +235,10 @@ class DirectoryWatcher:
|
|||||||
return
|
return
|
||||||
|
|
||||||
if self.current_process.returncode != 0:
|
if self.current_process.returncode != 0:
|
||||||
|
# Se falhar, tenta ler o resto do stderr
|
||||||
err_log = await self.current_process.stderr.read()
|
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")
|
state.update_task(fname, 'error', 0, label="Erro FFmpeg")
|
||||||
return
|
return
|
||||||
self.current_process = None
|
self.current_process = None
|
||||||
@@ -238,14 +277,11 @@ class DirectoryWatcher:
|
|||||||
def find_best_category(self, media_type, genre_ids, countries):
|
def find_best_category(self, media_type, genre_ids, countries):
|
||||||
"""
|
"""
|
||||||
Sistema de Pontuação 3.0 (Regras Estritas)
|
Sistema de Pontuação 3.0 (Regras Estritas)
|
||||||
Agora compara IDs e Siglas, não texto.
|
|
||||||
"""
|
"""
|
||||||
all_cats = list(Category.select())
|
all_cats = list(Category.select())
|
||||||
if not all_cats: return None
|
if not all_cats: return None
|
||||||
|
|
||||||
candidates = []
|
candidates = []
|
||||||
|
|
||||||
# 1. Filtro Rígido de TIPO (Movie vs Series)
|
|
||||||
for cat in all_cats:
|
for cat in all_cats:
|
||||||
if media_type == 'movie' and cat.content_type == 'series': continue
|
if media_type == 'movie' and cat.content_type == 'series': continue
|
||||||
if media_type != 'movie' and cat.content_type == 'movie': continue
|
if media_type != 'movie' and cat.content_type == 'movie': continue
|
||||||
@@ -257,53 +293,38 @@ class DirectoryWatcher:
|
|||||||
for cat in candidates:
|
for cat in candidates:
|
||||||
score = 0
|
score = 0
|
||||||
|
|
||||||
# Carrega filtros da categoria
|
|
||||||
cat_genres = cat.genre_filters.split(',') if cat.genre_filters else []
|
cat_genres = cat.genre_filters.split(',') if cat.genre_filters else []
|
||||||
cat_countries = cat.country_filters.split(',') if cat.country_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]
|
cat_countries = [c for c in cat_countries if c]
|
||||||
|
|
||||||
# --- Lógica de Correspondência ---
|
|
||||||
match_genre = False
|
match_genre = False
|
||||||
match_country = False
|
match_country = False
|
||||||
|
|
||||||
# Verifica Gêneros (Se a categoria tiver filtros, TEM que bater)
|
|
||||||
if cat_genres:
|
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):
|
if any(gid in cat_genres for gid in genre_ids):
|
||||||
match_genre = True
|
score += 50
|
||||||
score += 50 # Ganha muitos pontos por match específico
|
else:
|
||||||
|
score = -1000
|
||||||
|
else:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
if cat_countries:
|
||||||
|
if any(cc in cat_countries for cc in countries):
|
||||||
|
score += 50
|
||||||
else:
|
else:
|
||||||
# Se a categoria exige gênero e o arquivo não tem -> Categoria Descartada
|
|
||||||
score = -1000
|
score = -1000
|
||||||
else:
|
else:
|
||||||
# Categoria genérica de gênero (aceita tudo)
|
|
||||||
match_genre = True
|
|
||||||
score += 10 # Pontuação base baixa
|
|
||||||
|
|
||||||
# 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
|
|
||||||
else:
|
|
||||||
score = -1000 # Exige país mas não bate -> Descartada
|
|
||||||
else:
|
|
||||||
match_country = True
|
|
||||||
score += 10
|
score += 10
|
||||||
|
|
||||||
# Bônus se for categoria Mista (geralmente usada para Anime/Desenho)
|
|
||||||
if cat.content_type == 'mixed' and score > 0:
|
if cat.content_type == 'mixed' and score > 0:
|
||||||
score += 5
|
score += 5
|
||||||
|
|
||||||
scored_cats.append((score, cat))
|
scored_cats.append((score, cat))
|
||||||
|
|
||||||
# Ordena
|
|
||||||
scored_cats.sort(key=lambda x: x[0], reverse=True)
|
scored_cats.sort(key=lambda x: x[0], reverse=True)
|
||||||
best_match = scored_cats[0]
|
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:
|
if best_match[0] < 0:
|
||||||
for cat in candidates:
|
for cat in candidates:
|
||||||
if not cat.genre_filters and not cat.country_filters:
|
if not cat.genre_filters and not cat.country_filters:
|
||||||
|
|||||||
Reference in New Issue
Block a user