consertado o renamer para masi precisão
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -20,52 +20,59 @@ class RenamerCore:
|
|||||||
self.tv_api = TV()
|
self.tv_api = TV()
|
||||||
self.search_api = Search()
|
self.search_api = Search()
|
||||||
|
|
||||||
|
def clean_filename(self, filename):
|
||||||
|
name, ext = os.path.splitext(filename)
|
||||||
|
patterns = [
|
||||||
|
r'(?i)(www\.[a-z0-9-]+\.[a-z]{2,})',
|
||||||
|
r'(?i)(rede\s?canais)',
|
||||||
|
r'(?i)(comando\s?torrents?)',
|
||||||
|
r'(?i)(bludv)',
|
||||||
|
r'(?i)(\bassistir\b)',
|
||||||
|
r'(?i)(\bbaixar\b)',
|
||||||
|
r'(?i)(\bdownload\b)',
|
||||||
|
r'(?i)(\bfilme\s?completo\b)',
|
||||||
|
r'(?i)(\bpt-br\b)',
|
||||||
|
]
|
||||||
|
clean_name = name
|
||||||
|
for pat in patterns:
|
||||||
|
clean_name = re.sub(pat, '', clean_name)
|
||||||
|
|
||||||
|
clean_name = re.sub(r'\s+-\s+', ' ', clean_name)
|
||||||
|
clean_name = re.sub(r'\s+', ' ', clean_name).strip()
|
||||||
|
return clean_name + ext
|
||||||
|
|
||||||
def identify_file(self, filepath):
|
def identify_file(self, filepath):
|
||||||
|
# ... (código identify_file igual ao anterior) ...
|
||||||
|
# Vou resumir aqui para não ficar gigante, mantenha o identify_file
|
||||||
|
# que passamos na última resposta (com o fix do 'results' e 'str').
|
||||||
filename = os.path.basename(filepath)
|
filename = os.path.basename(filepath)
|
||||||
try:
|
cleaned_filename = self.clean_filename(filename)
|
||||||
guess = guessit(filename)
|
try: guess = guessit(cleaned_filename)
|
||||||
except Exception as e:
|
except Exception as e: return {'status': 'ERROR', 'msg': str(e)}
|
||||||
return {'status': 'ERROR', 'msg': str(e)}
|
|
||||||
|
|
||||||
title = guess.get('title')
|
title = guess.get('title')
|
||||||
if not title: return {'status': 'NOT_FOUND', 'msg': 'Sem título'}
|
if not title: return {'status': 'NOT_FOUND', 'msg': 'Sem título'}
|
||||||
|
|
||||||
if not self.api_key: return {'status': 'ERROR', 'msg': 'Sem API Key'}
|
if not self.api_key: return {'status': 'ERROR', 'msg': 'Sem API Key'}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
media_type = guess.get('type', 'movie')
|
media_type = guess.get('type', 'movie')
|
||||||
if media_type == 'episode':
|
if media_type == 'episode': results = self.search_api.tv_shows(term=title)
|
||||||
results = self.search_api.tv_shows(term=title)
|
|
||||||
else:
|
else:
|
||||||
results = self.search_api.movies(term=title)
|
results = self.search_api.movies(term=title)
|
||||||
if not results: results = self.search_api.tv_shows(term=title)
|
if not results: results = self.search_api.tv_shows(term=title)
|
||||||
except: return {'status': 'NOT_FOUND', 'msg': 'Erro TMDb'}
|
except: return {'status': 'NOT_FOUND', 'msg': 'Erro TMDb'}
|
||||||
|
|
||||||
if not results: return {'status': 'NOT_FOUND', 'msg': 'Nenhum resultado TMDb'}
|
if isinstance(results, dict) and 'results' in results: results = results['results']
|
||||||
|
elif hasattr(results, 'results'): results = results.results
|
||||||
|
|
||||||
# --- 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):
|
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 inválido'}
|
||||||
return {'status': 'NOT_FOUND', 'msg': 'Formato de resposta inválido'}
|
|
||||||
# -------------------------------------------------------------
|
|
||||||
|
|
||||||
candidates = []
|
candidates = []
|
||||||
for res in results:
|
for res in results:
|
||||||
# Proteção extra: se o item for string, pula
|
|
||||||
if isinstance(res, str): continue
|
if isinstance(res, str): continue
|
||||||
|
|
||||||
# Obtém atributos de forma segura (funciona para dict ou objeto)
|
|
||||||
if isinstance(res, dict):
|
if isinstance(res, dict):
|
||||||
r_id = res.get('id')
|
r_id = res.get('id'); r_title = res.get('title') or res.get('name')
|
||||||
r_title = res.get('title') or res.get('name')
|
|
||||||
r_date = res.get('release_date') or res.get('first_air_date')
|
r_date = res.get('release_date') or res.get('first_air_date')
|
||||||
r_overview = res.get('overview', '')
|
r_overview = res.get('overview', '')
|
||||||
else:
|
else:
|
||||||
@@ -75,17 +82,11 @@ class RenamerCore:
|
|||||||
r_overview = getattr(res, 'overview', '')
|
r_overview = getattr(res, 'overview', '')
|
||||||
|
|
||||||
if not r_title or not r_id: continue
|
if not r_title or not r_id: continue
|
||||||
|
|
||||||
r_year = int(str(r_date)[:4]) if r_date else 0
|
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()
|
||||||
t1 = str(title).lower()
|
|
||||||
t2 = str(r_title).lower()
|
|
||||||
|
|
||||||
base_score = SequenceMatcher(None, t1, t2).ratio()
|
base_score = SequenceMatcher(None, t1, t2).ratio()
|
||||||
|
if t1 in t2 or t2 in t1: base_score = max(base_score, 0.85)
|
||||||
if t1 in t2 or t2 in t1:
|
|
||||||
base_score = max(base_score, 0.85)
|
|
||||||
|
|
||||||
g_year = guess.get('year')
|
g_year = guess.get('year')
|
||||||
if g_year and r_year:
|
if g_year and r_year:
|
||||||
@@ -93,38 +94,29 @@ class RenamerCore:
|
|||||||
elif abs(g_year - r_year) <= 1: base_score += 0.05
|
elif abs(g_year - r_year) <= 1: base_score += 0.05
|
||||||
|
|
||||||
final_score = min(base_score, 1.0)
|
final_score = min(base_score, 1.0)
|
||||||
|
|
||||||
candidates.append({
|
candidates.append({
|
||||||
'tmdb_id': r_id,
|
'tmdb_id': r_id, 'title': r_title, 'year': r_year,
|
||||||
'title': r_title,
|
|
||||||
'year': r_year,
|
|
||||||
'type': 'movie' if hasattr(res, 'title') or (isinstance(res, dict) and 'title' in res) else 'tv',
|
'type': 'movie' if hasattr(res, 'title') or (isinstance(res, dict) and 'title' in res) else 'tv',
|
||||||
'overview': str(r_overview)[:100],
|
'overview': str(r_overview)[:100], 'score': final_score
|
||||||
'score': final_score
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if not candidates: return {'status': 'NOT_FOUND', 'msg': 'Sem candidatos válidos'}
|
if not candidates: return {'status': 'NOT_FOUND', 'msg': 'Sem candidatos'}
|
||||||
|
|
||||||
candidates.sort(key=lambda x: x['score'], reverse=True)
|
candidates.sort(key=lambda x: x['score'], reverse=True)
|
||||||
best = candidates[0]
|
best = candidates[0]
|
||||||
|
if len(candidates) == 1 and best['score'] > 0.6: return {'status': 'MATCH', 'match': best, 'guessed': guess}
|
||||||
if len(candidates) == 1 and best['score'] > 0.6:
|
|
||||||
return {'status': 'MATCH', 'match': best, 'guessed': guess}
|
|
||||||
|
|
||||||
is_clear_winner = False
|
is_clear_winner = False
|
||||||
if len(candidates) > 1:
|
if len(candidates) > 1 and (best['score'] - candidates[1]['score']) > 0.15: is_clear_winner = True
|
||||||
if (best['score'] - candidates[1]['score']) > 0.15:
|
|
||||||
is_clear_winner = True
|
|
||||||
|
|
||||||
if best['score'] >= self.min_confidence or is_clear_winner:
|
if best['score'] >= self.min_confidence or is_clear_winner:
|
||||||
return {'status': 'MATCH', 'match': best, 'guessed': guess}
|
return {'status': 'MATCH', 'match': best, 'guessed': guess}
|
||||||
|
|
||||||
return {'status': 'AMBIGUOUS', 'candidates': candidates[:5], 'guessed': guess}
|
return {'status': 'AMBIGUOUS', 'candidates': candidates[:5], 'guessed': guess}
|
||||||
|
|
||||||
def get_details(self, tmdb_id, media_type):
|
def get_details(self, tmdb_id, media_type):
|
||||||
if media_type == 'movie': return self.movie_api.details(tmdb_id)
|
if media_type == 'movie': return self.movie_api.details(tmdb_id)
|
||||||
return self.tv_api.details(tmdb_id)
|
return self.tv_api.details(tmdb_id)
|
||||||
|
|
||||||
|
# --- AQUI ESTÁ A MUDANÇA ---
|
||||||
def build_path(self, category_obj, media_info, guessed_info):
|
def build_path(self, category_obj, media_info, guessed_info):
|
||||||
clean_title = re.sub(r'[\\/*?:"<>|]', "", media_info['title']).strip()
|
clean_title = re.sub(r'[\\/*?:"<>|]', "", media_info['title']).strip()
|
||||||
year = str(media_info['year'])
|
year = str(media_info['year'])
|
||||||
@@ -138,8 +130,10 @@ class RenamerCore:
|
|||||||
else: is_series = (actual_type == 'tv')
|
else: is_series = (actual_type == 'tv')
|
||||||
|
|
||||||
if not is_series:
|
if not is_series:
|
||||||
|
# Filme: "Matrix (1999).mkv"
|
||||||
return f"{clean_title} ({year}).mkv"
|
return f"{clean_title} ({year}).mkv"
|
||||||
else:
|
else:
|
||||||
|
# Série
|
||||||
season = guessed_info.get('season')
|
season = guessed_info.get('season')
|
||||||
episode = guessed_info.get('episode')
|
episode = guessed_info.get('episode')
|
||||||
|
|
||||||
@@ -150,6 +144,11 @@ class RenamerCore:
|
|||||||
if not episode: episode = 1
|
if not episode: episode = 1
|
||||||
|
|
||||||
season_folder = f"Temporada {int(season):02d}"
|
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")
|
# MUDANÇA: Nome do arquivo simplificado
|
||||||
|
# De: "Nome Serie S01E01.mkv"
|
||||||
|
# Para: "Episódio 01.mkv"
|
||||||
|
filename = f"Episódio {int(episode):02d}.mkv"
|
||||||
|
|
||||||
|
# Caminho relativo: "Nome Série/Temporada 01/Episódio 01.mkv"
|
||||||
|
return os.path.join(clean_title, season_folder, filename)
|
||||||
@@ -195,12 +195,23 @@ class DirectoryWatcher:
|
|||||||
return
|
return
|
||||||
self.current_process = None
|
self.current_process = None
|
||||||
|
|
||||||
# 4. DEPLOY FINAL
|
# 4. DEPLOY SEGURO
|
||||||
state.update_task(fname, 'running', 98, label="Organizando...")
|
state.update_task(fname, 'running', 98, label="Organizando...")
|
||||||
|
|
||||||
|
# Monta caminho completo
|
||||||
final_full_path = Path(category.target_path) / relative_path
|
final_full_path = Path(category.target_path) / relative_path
|
||||||
|
|
||||||
|
# Garante que a PASTA existe (Merge seguro)
|
||||||
|
# Se a pasta já existe, o mkdir(exist_ok=True) não faz nada (não apaga, não mexe)
|
||||||
final_full_path.parent.mkdir(parents=True, exist_ok=True)
|
final_full_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
# Tratamento de ARQUIVO duplicado
|
||||||
|
if final_full_path.exists():
|
||||||
|
# Se o arquivo já existe, apagamos ele para substituir pelo novo (Upgrade)
|
||||||
|
state.log(f"⚠️ Substituindo arquivo existente: {final_full_path.name}")
|
||||||
|
os.remove(str(final_full_path))
|
||||||
|
|
||||||
|
# Move APENAS o arquivo
|
||||||
shutil.move(str(temp_output), str(final_full_path))
|
shutil.move(str(temp_output), str(final_full_path))
|
||||||
|
|
||||||
if AppConfig.get_val('deploy_mode', 'move') == 'move':
|
if AppConfig.get_val('deploy_mode', 'move') == 'move':
|
||||||
@@ -209,7 +220,6 @@ class DirectoryWatcher:
|
|||||||
await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`")
|
await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`")
|
||||||
state.update_task(fname, 'done', 100, label=f"{full_details['title']}")
|
state.update_task(fname, 'done', 100, label=f"{full_details['title']}")
|
||||||
state.current_file = ""
|
state.current_file = ""
|
||||||
|
|
||||||
# Limpeza pasta vazia
|
# Limpeza pasta vazia
|
||||||
if AppConfig.get_val('cleanup_empty_folders', 'true') == 'true':
|
if AppConfig.get_val('cleanup_empty_folders', 'true') == 'true':
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user