diff --git a/app/core/__pycache__/renamer.cpython-311.pyc b/app/core/__pycache__/renamer.cpython-311.pyc index 78b4429..cf06fd8 100644 Binary files a/app/core/__pycache__/renamer.cpython-311.pyc and b/app/core/__pycache__/renamer.cpython-311.pyc differ diff --git a/app/core/__pycache__/watcher.cpython-311.pyc b/app/core/__pycache__/watcher.cpython-311.pyc index bdbaefe..0906c13 100644 Binary files a/app/core/__pycache__/watcher.cpython-311.pyc and b/app/core/__pycache__/watcher.cpython-311.pyc differ diff --git a/app/core/renamer.py b/app/core/renamer.py index f0d8c61..fe317f7 100755 --- a/app/core/renamer.py +++ b/app/core/renamer.py @@ -20,52 +20,59 @@ class RenamerCore: self.tv_api = TV() 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): + # ... (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) - try: - guess = guessit(filename) - except Exception as e: - return {'status': 'ERROR', 'msg': str(e)} + cleaned_filename = self.clean_filename(filename) + try: guess = guessit(cleaned_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) + 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 + if isinstance(results, dict) and 'results' in results: results = results['results'] + 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'} - # ------------------------------------------------------------- + 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