This commit is contained in:
2026-02-08 23:07:50 +00:00
commit 95a9bcd24d
32 changed files with 1415 additions and 0 deletions

155
app/core/renamer.py Executable file
View File

@@ -0,0 +1,155 @@
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")