From e273233e3861c8d9178240f4d10f1775b8e28a37 Mon Sep 17 00:00:00 2001 From: Creidsu Date: Sun, 8 Feb 2026 23:32:18 +0000 Subject: [PATCH] =?UTF-8?q?consertado=20o=20renamer=20para=20masi=20precis?= =?UTF-8?q?=C3=A3o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/core/__pycache__/renamer.cpython-311.pyc | Bin 8264 -> 9025 bytes app/core/__pycache__/watcher.cpython-311.pyc | Bin 13937 -> 14221 bytes app/core/renamer.py | 101 +++++++++---------- app/core/watcher.py | 16 ++- 4 files changed, 63 insertions(+), 54 deletions(-) diff --git a/app/core/__pycache__/renamer.cpython-311.pyc b/app/core/__pycache__/renamer.cpython-311.pyc index 78b4429c15a8cecb33a70af3053a7107292f3a70..cf06fd8adb9454b90219edef208ce2d401c6a06c 100644 GIT binary patch delta 4123 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 QJNXeT{so6@krFV}0Vx@!Z~y=R delta 397 zcmeCp|CqzKoR^o20SMl`Y0u z94wbBYE#3RA}R?JWSGs6Vmp_4axb6E zPfiq(nS5IQFr&-leg$Jj*U5(!Oc~ua|5Wg1+FYP~gqe|Tv#I({CdQ!6?=_Q|C7#MD zEwH$vY<^MB;)@z9v%KSc%&!i=}Iy` 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'} - # ------------------------------------------------------------- + return {'status': 'NOT_FOUND', 'msg': 'Formato 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_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: @@ -75,17 +82,11 @@ class RenamerCore: 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() - + 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) + 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: @@ -93,38 +94,29 @@ class RenamerCore: 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, + '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 + 'overview': str(r_overview)[:100], '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) 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 - if len(candidates) > 1: - if (best['score'] - candidates[1]['score']) > 0.15: - is_clear_winner = True + if len(candidates) > 1 and (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) + # --- AQUI ESTÁ A MUDANÇA --- def build_path(self, category_obj, media_info, guessed_info): clean_title = re.sub(r'[\\/*?:"<>|]', "", media_info['title']).strip() year = str(media_info['year']) @@ -138,8 +130,10 @@ class RenamerCore: else: is_series = (actual_type == 'tv') if not is_series: + # Filme: "Matrix (1999).mkv" return f"{clean_title} ({year}).mkv" else: + # Série season = guessed_info.get('season') episode = guessed_info.get('episode') @@ -150,6 +144,11 @@ class RenamerCore: 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") \ No newline at end of file + # 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) \ No newline at end of file diff --git a/app/core/watcher.py b/app/core/watcher.py index 70cb327..f19483d 100644 --- a/app/core/watcher.py +++ b/app/core/watcher.py @@ -195,12 +195,23 @@ class DirectoryWatcher: return self.current_process = None - # 4. DEPLOY FINAL +# 4. DEPLOY SEGURO state.update_task(fname, 'running', 98, label="Organizando...") + # Monta caminho completo 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) + # 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)) if AppConfig.get_val('deploy_mode', 'move') == 'move': @@ -209,8 +220,7 @@ class DirectoryWatcher: await self.bot.send_notification(f"🎬 Organizado: `{full_details['title']}`") state.update_task(fname, 'done', 100, label=f"{full_details['title']}") state.current_file = "" - - # Limpeza pasta vazia + # Limpeza pasta vazia if AppConfig.get_val('cleanup_empty_folders', 'true') == 'true': try: parent = filepath.parent