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")