Files
clei-flow/app/core/renamer.py
2026-02-08 23:07:50 +00:00

155 lines
6.1 KiB
Python
Executable File

import os
import re
from guessit import guessit
from tmdbv3api import TMDb, Movie, TV, Search
from database import AppConfig
from difflib import SequenceMatcher
class RenamerCore:
def __init__(self):
self.api_key = AppConfig.get_val('tmdb_api_key')
self.lang = AppConfig.get_val('tmdb_language', 'pt-BR')
self.min_confidence = int(AppConfig.get_val('min_confidence', '90')) / 100.0
self.tmdb = TMDb()
if self.api_key:
self.tmdb.api_key = self.api_key
self.tmdb.language = self.lang
self.movie_api = Movie()
self.tv_api = TV()
self.search_api = Search()
def identify_file(self, filepath):
filename = os.path.basename(filepath)
try:
guess = guessit(filename)
except Exception as e:
return {'status': 'ERROR', 'msg': str(e)}
title = guess.get('title')
if not title: return {'status': 'NOT_FOUND', 'msg': 'Sem título'}
if not self.api_key: return {'status': 'ERROR', 'msg': 'Sem API Key'}
try:
media_type = guess.get('type', 'movie')
if media_type == 'episode':
results = self.search_api.tv_shows(term=title)
else:
results = self.search_api.movies(term=title)
if not results: results = self.search_api.tv_shows(term=title)
except: return {'status': 'NOT_FOUND', 'msg': 'Erro TMDb'}
if not results: return {'status': 'NOT_FOUND', 'msg': 'Nenhum resultado TMDb'}
# --- CORREÇÃO DE SEGURANÇA (O erro 'str object' estava aqui) ---
# Se o TMDb retornou um dicionário (paginado), pegamos a lista dentro dele.
if isinstance(results, dict) and 'results' in results:
results = results['results']
# Se retornou um objeto que tem atributo 'results', usamos ele
elif hasattr(results, 'results'):
results = results.results
# Se ainda assim for uma lista de strings (chaves), aborta
if results and isinstance(results, list) and len(results) > 0 and isinstance(results[0], str):
# Isso acontece se iterou sobre chaves de um dict sem querer
return {'status': 'NOT_FOUND', 'msg': 'Formato de resposta inválido'}
# -------------------------------------------------------------
candidates = []
for res in results:
# Proteção extra: se o item for string, pula
if isinstance(res, str): continue
# Obtém atributos de forma segura (funciona para dict ou objeto)
if isinstance(res, dict):
r_id = res.get('id')
r_title = res.get('title') or res.get('name')
r_date = res.get('release_date') or res.get('first_air_date')
r_overview = res.get('overview', '')
else:
r_id = getattr(res, 'id', None)
r_title = getattr(res, 'title', getattr(res, 'name', ''))
r_date = getattr(res, 'release_date', getattr(res, 'first_air_date', ''))
r_overview = getattr(res, 'overview', '')
if not r_title or not r_id: continue
r_year = int(str(r_date)[:4]) if r_date else 0
# Score e Comparação
t1 = str(title).lower()
t2 = str(r_title).lower()
base_score = SequenceMatcher(None, t1, t2).ratio()
if t1 in t2 or t2 in t1:
base_score = max(base_score, 0.85)
g_year = guess.get('year')
if g_year and r_year:
if g_year == r_year: base_score += 0.15
elif abs(g_year - r_year) <= 1: base_score += 0.05
final_score = min(base_score, 1.0)
candidates.append({
'tmdb_id': r_id,
'title': r_title,
'year': r_year,
'type': 'movie' if hasattr(res, 'title') or (isinstance(res, dict) and 'title' in res) else 'tv',
'overview': str(r_overview)[:100],
'score': final_score
})
if not candidates: return {'status': 'NOT_FOUND', 'msg': 'Sem candidatos válidos'}
candidates.sort(key=lambda x: x['score'], reverse=True)
best = candidates[0]
if len(candidates) == 1 and best['score'] > 0.6:
return {'status': 'MATCH', 'match': best, 'guessed': guess}
is_clear_winner = False
if len(candidates) > 1:
if (best['score'] - candidates[1]['score']) > 0.15:
is_clear_winner = True
if best['score'] >= self.min_confidence or is_clear_winner:
return {'status': 'MATCH', 'match': best, 'guessed': guess}
return {'status': 'AMBIGUOUS', 'candidates': candidates[:5], 'guessed': guess}
def get_details(self, tmdb_id, media_type):
if media_type == 'movie': return self.movie_api.details(tmdb_id)
return self.tv_api.details(tmdb_id)
def build_path(self, category_obj, media_info, guessed_info):
clean_title = re.sub(r'[\\/*?:"<>|]', "", media_info['title']).strip()
year = str(media_info['year'])
forced_type = category_obj.content_type
actual_type = media_info['type']
is_series = False
if forced_type == 'series': is_series = True
elif forced_type == 'movie': is_series = False
else: is_series = (actual_type == 'tv')
if not is_series:
return f"{clean_title} ({year}).mkv"
else:
season = guessed_info.get('season')
episode = guessed_info.get('episode')
if isinstance(season, list): season = season[0]
if isinstance(episode, list): episode = episode[0]
if not season: season = 1
if not episode: episode = 1
season_folder = f"Temporada {int(season):02d}"
file_suffix = f"S{int(season):02d}E{int(episode):02d}"
return os.path.join(clean_title, season_folder, f"{clean_title} {file_suffix}.mkv")