From 3ebe723edb5e86a4700a2a0c9b15b3866aa7443f Mon Sep 17 00:00:00 2001 From: Creidsu Date: Sun, 1 Feb 2026 17:39:27 +0000 Subject: [PATCH] atualizado para separa desenhos filmes animes e series --- .../__pycache__/renamer.cpython-310.pyc | Bin 14974 -> 16204 bytes app/modules/renamer.py | 309 ++++++++++-------- data/status.json | 2 +- 3 files changed, 174 insertions(+), 137 deletions(-) diff --git a/app/modules/__pycache__/renamer.cpython-310.pyc b/app/modules/__pycache__/renamer.cpython-310.pyc index 370723ae72a973f172e31e36f81df842fe17ce57..c3d412132f722b73ebd77f907607c846124be8f2 100644 GIT binary patch literal 16204 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 delta 7967 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 MGakmZ@ls6xU&-IessI20 diff --git a/app/modules/renamer.py b/app/modules/renamer.py index 65d1959..9e93e0c 100755 --- a/app/modules/renamer.py +++ b/app/modules/renamer.py @@ -11,8 +11,12 @@ from guessit import guessit # 1. CONFIGURAÇÕES E CONSTANTES # ============================================================================== ROOT_DIR = "/downloads" +DEST_DIR = os.path.join(ROOT_DIR, "preparados") # Nova raiz de destino CONFIG_FILE = 'config.json' +# ID do Gênero Animação no TMDb +GENRE_ANIMATION = 16 + # Extensões para proteger pastas de exclusão VIDEO_EXTENSIONS = {'.mkv', '.mp4', '.avi', '.mov', '.iso', '.wmv', '.flv', '.webm', '.m4v'} # Extensões de legenda para mover junto @@ -57,12 +61,11 @@ class MediaOrganizer: ui.notify('API Key salva com sucesso!', type='positive') async def search_tmdb(self, title, year, media_type): - """Consulta o TMDb e retorna candidatos.""" + """Consulta o TMDb e retorna candidatos com GÊNEROS.""" if not self.api_key: return [] search = tmdb.Search() try: - # Roda em thread separada para não travar a UI loop = asyncio.get_event_loop() if media_type == 'movie': # Busca Filmes @@ -76,6 +79,36 @@ class MediaOrganizer: print(f"Erro TMDb: {e}") return [] + def detect_category(self, match_data, media_type): + """ + Define a categoria baseada nos metadados do TMDb. + Retorna: 'Filmes', 'Séries', 'Animes' ou 'Desenhos' + """ + if not match_data: + # Fallback se não tiver match + return 'Filmes' if media_type == 'movie' else 'Séries' + + genre_ids = match_data.get('genre_ids', []) + + # É ANIMAÇÃO? (ID 16) + if GENRE_ANIMATION in genre_ids: + # Verifica origem para diferenciar Anime de Desenho + original_lang = match_data.get('original_language', '') + origin_countries = match_data.get('origin_country', []) # Lista (comum em TV) + + is_asian_lang = original_lang in ['ja', 'ko'] + is_asian_country = any(c in ['JP', 'KR'] for c in origin_countries) + + # Se for animação japonesa ou coreana -> Animes + if is_asian_lang or is_asian_country: + return 'Animes' + + # Caso contrário (Disney, Pixar, Cartoon Network) -> Desenhos + return 'Desenhos' + + # NÃO É ANIMAÇÃO (Live Action) + return 'Filmes' if media_type == 'movie' else 'Séries' + async def analyze_folder(self): """Analisa a pasta usando Guessit + TMDb.""" self.preview_data = [] @@ -85,17 +118,17 @@ class MediaOrganizer: ui.notify('Por favor, configure a API Key do TMDb primeiro.', type='negative') return - # Notificação de progresso - loading = ui.notification(message='Analisando arquivos (Guessit + TMDb)...', spinner=True, timeout=None) + loading = ui.notification(message='Analisando arquivos (Guessit + TMDb + Categorias)...', spinner=True, timeout=None) try: - # Cria pastas base se não existirem - os.makedirs(os.path.join(ROOT_DIR, "Filmes"), exist_ok=True) - os.makedirs(os.path.join(ROOT_DIR, "Séries"), exist_ok=True) + # Cria a estrutura base de destino + categories = ['Filmes', 'Séries', 'Animes', 'Desenhos'] + for cat in categories: + os.makedirs(os.path.join(DEST_DIR, cat), exist_ok=True) for root, dirs, files in os.walk(self.path): - # Ignora pastas de destino para evitar loop - if "Filmes" in root or "Séries" in root: continue + # Ignora a pasta de destino "preparados" para não processar o que já foi feito + if "preparados" in root: continue files_in_dir = set(files) @@ -107,40 +140,34 @@ class MediaOrganizer: guess = guessit(file) title = guess.get('title') year = guess.get('year') - media_type = guess.get('type') # 'movie' ou 'episode' + media_type = guess.get('type') - if not title: continue # Se não achou nem título, ignora + if not title: continue # 2. Consulta TMDb candidates = await self.search_tmdb(title, year, media_type) - # 3. Lógica de Decisão (Match ou Ambiguidade) + # 3. Lógica de Decisão match_data = None - status = 'AMBIGUO' # Padrão: incerto + status = 'AMBIGUO' if candidates: - # Tenta match exato (Primeiro resultado geralmente é o melhor no TMDb) first = candidates[0] - tmdb_title = first.get('title') if media_type == 'movie' else first.get('name') tmdb_date = first.get('release_date') if media_type == 'movie' else first.get('first_air_date') tmdb_year = int(tmdb_date[:4]) if tmdb_date else None - # Se ano bate ou não tem ano no arquivo original, confia no primeiro resultado if year and tmdb_year == year: match_data = first status = 'OK' elif not year: - # Sem ano no arquivo, mas achou resultado. Marca como Ambíguo mas sugere o primeiro match_data = first - status = 'CHECK' # Requer atenção visual + status = 'CHECK' else: - # Ano diferente. Pode ser remake ou erro. match_data = first status = 'CHECK' else: status = 'NAO_ENCONTRADO' - # Cria objeto de item item = { 'id': len(self.preview_data), 'original_file': file, @@ -148,24 +175,24 @@ class MediaOrganizer: 'guess_title': title, 'guess_year': year, 'type': media_type, - 'candidates': candidates, # Lista para o modal de escolha + 'candidates': candidates, 'selected_match': match_data, 'status': status, - 'target_path': None, # Será calculado - 'subtitles': [] # Lista de legendas associadas + 'target_path': None, + 'category': None, # Nova propriedade + 'subtitles': [] } - # Calcula caminho se tiver match if match_data: self.calculate_path(item) - # Procura Legendas Associadas + # Procura Legendas video_stem = Path(file).stem for f in files_in_dir: if f == file: continue if os.path.splitext(f)[1].lower() in SUBTITLE_EXTENSIONS: if f.startswith(video_stem): - suffix = f[len(video_stem):] # Ex: .forced.srt + suffix = f[len(video_stem):] item['subtitles'].append({ 'original': f, 'suffix': suffix, @@ -181,15 +208,14 @@ class MediaOrganizer: loading.dismiss() - # Se achou itens, muda visualização if self.preview_data: return True else: - ui.notify('Nenhum vídeo encontrado nesta pasta.', type='warning') + ui.notify('Nenhum vídeo novo encontrado.', type='warning') return False def calculate_path(self, item): - """Gera o caminho final baseado no match selecionado.""" + """Gera o caminho baseado na Categoria e no Tipo.""" match = item['selected_match'] if not match: item['target_path'] = None @@ -197,40 +223,57 @@ class MediaOrganizer: ext = os.path.splitext(item['original_file'])[1] + # 1. Determinar Categoria (Filmes, Séries, Animes, Desenhos) + category = self.detect_category(match, item['type']) + item['category'] = category # Salva para mostrar na UI se quiser + + # Nome base limpo + title_raw = match.get('title') if item['type'] == 'movie' else match.get('name') + title_raw = title_raw or item['guess_title'] + final_title = title_raw.replace('/', '-').replace(':', '-').strip() + + date = match.get('release_date') if item['type'] == 'movie' else match.get('first_air_date') + year_str = date[:4] if date else '0000' + + # 2. Lógica de Pasta baseada na Categoria + base_folder = os.path.join(DEST_DIR, category) + + # CASO 1: É FILME (Seja Live Action, Anime Movie ou Desenho Movie) if item['type'] == 'movie': - # Estrutura: /downloads/Filmes/Nome (Ano).ext - title = match.get('title', item['guess_title']).replace('/', '-').replace(':', '-') - date = match.get('release_date', '') - year = date[:4] if date else '0000' - - new_name = f"{title} ({year}){ext}" - item['target_path'] = os.path.join(ROOT_DIR, "Filmes", new_name) + new_filename = f"{final_title} ({year_str}){ext}" + if category == 'Filmes': + # Filmes Live Action -> Pasta Própria (opcional, aqui pus arquivo direto na pasta Filmes) + # Se quiser pasta por filme: os.path.join(base_folder, f"{final_title} ({year_str})", new_filename) + item['target_path'] = os.path.join(base_folder, new_filename) + else: + # Animes/Desenhos -> Arquivo solto na raiz da categoria (Misto) + item['target_path'] = os.path.join(base_folder, new_filename) + + # CASO 2: É SÉRIE (Seja Live Action, Anime Serie ou Desenho Serie) else: - # Estrutura: /downloads/Séries/Nome/Temporada XX/Episódio YY.ext - name = match.get('name', item['guess_title']).replace('/', '-').replace(':', '-') - - # Tenta pegar temporada/episódio do guessit - # Se falhar, usa S00E00 como fallback seguro para não perder arquivo + # Séries sempre precisam de estrutura de pasta guess = guessit(item['original_file']) s = guess.get('season') e = guess.get('episode') if not s or not e: - # Caso extremo: não achou temporada/ep no nome do arquivo - item['status'] = 'ERRO_S_E' # Erro de Season/Episode + item['status'] = 'ERRO_S_E' item['target_path'] = None return - # Suporte a múltiplas temporadas/episodios (lista) if isinstance(s, list): s = s[0] if isinstance(e, list): e = e[0] s_fmt = f"{s:02d}" e_fmt = f"{e:02d}" + # Caminho: Categoria / Nome da Série / Temporada XX / Episódio.ext item['target_path'] = os.path.join( - ROOT_DIR, "Séries", name, f"Temporada {s_fmt}", f"Episódio {e_fmt}{ext}" + base_folder, + final_title, + f"Temporada {s_fmt}", + f"Episódio {e_fmt}{ext}" # Renomeia o EP para padrão limpo ) async def execute_move(self): @@ -239,16 +282,14 @@ class MediaOrganizer: n = ui.notification('Organizando biblioteca...', spinner=True, timeout=None) for item in self.preview_data: - # Só move se tiver Status OK ou CHECK (confirmado pelo usuário) e tiver destino if not item['target_path'] or item['status'] == 'NAO_ENCONTRADO': continue try: - # 1. Mover Vídeo + # Mover Vídeo src = os.path.join(item['original_root'], item['original_file']) dst = item['target_path'] - # Verifica colisão if os.path.exists(dst): ui.notify(f"Pulei {os.path.basename(dst)} (Já existe)", type='warning') continue @@ -257,27 +298,24 @@ class MediaOrganizer: shutil.move(src, dst) moved += 1 - # 2. Mover Legendas - video_dst_stem = os.path.splitext(dst)[0] # Caminho sem extensão + # Mover Legendas + video_dst_stem = os.path.splitext(dst)[0] for sub in item['subtitles']: - sub_dst = video_dst_stem + sub['suffix'] # Ex: /path/Filme.forced.srt + sub_dst = video_dst_stem + sub['suffix'] if not os.path.exists(sub_dst): shutil.move(sub['src'], sub_dst) except Exception as e: ui.notify(f"Erro ao mover {item['original_file']}: {e}", type='negative') - # 3. Limpeza Segura + # Limpeza cleaned = 0 sorted_folders = sorted(list(self.folders_to_clean), key=len, reverse=True) for folder in sorted_folders: - if not os.path.exists(folder) or folder == ROOT_DIR: continue + if not os.path.exists(folder) or folder == ROOT_DIR or "preparados" in folder: continue try: - # Verifica se sobrou vídeo remaining = [f for f in os.listdir(folder) if os.path.splitext(f)[1].lower() in VIDEO_EXTENSIONS] - - # Verifica se sobrou subpasta não vazia has_subfolder = any(os.path.isdir(os.path.join(folder, f)) for f in os.listdir(folder)) if not remaining and not has_subfolder: @@ -286,93 +324,88 @@ class MediaOrganizer: except: pass n.dismiss() - ui.notify(f'{moved} arquivos organizados. {cleaned} pastas limpas.', type='positive') + ui.notify(f'{moved} arquivos organizados em "preparados". {cleaned} pastas limpas.', type='positive') return True # ============================================================================== -# 4. INTERFACE GRÁFICA (NiceGUI) - CORRIGIDA +# 4. INTERFACE GRÁFICA # ============================================================================== def create_ui(): organizer = MediaOrganizer() - # Container principal que envolve tudo (substitui o layout de página) with ui.column().classes('w-full h-full p-0 gap-0'): - - # --- FALSO HEADER (ui.row em vez de ui.header) --- - # Agora ele pode viver dentro de uma TabPanel sem dar erro - with ui.row().classes('w-full bg-blue-900 text-white items-center p-2 shadow-md'): - ui.icon('movie_filter', size='md') - ui.label('Media Organizer Pro').classes('text-lg font-bold ml-2') + # Header + with ui.row().classes('w-full bg-indigo-900 text-white items-center p-3 shadow-md'): + ui.icon('smart_display', size='md') + ui.label('Media Organizer v2').classes('text-lg font-bold ml-2') + ui.label('(Filmes • Séries • Animes • Desenhos)').classes('text-xs text-gray-300 ml-1 mt-1') ui.space() - # Campo API Key with ui.row().classes('items-center gap-2'): key_input = ui.input('TMDb API Key', password=True).props('dense dark input-class=text-white outlined').classes('w-64') key_input.value = organizer.api_key ui.button(icon='save', on_click=lambda: organizer.set_api_key(key_input.value)).props('flat dense round color=white') - # --- CONTAINER DE CONTEÚDO --- main_content = ui.column().classes('w-full p-4 gap-4') - # --- DIALOGO DE RESOLUÇÃO MANUAL (Mantém igual) --- + # Dialogo resolution_dialog = ui.dialog() def open_resolution_dialog(item, row_refresh_callback): with resolution_dialog, ui.card().classes('w-full max-w-4xl'): - ui.label(f"Resolvendo: {item['original_file']}").classes('text-lg font-bold') - ui.label(f"Identificado como: {item['guess_title']} ({item['guess_year']})").classes('text-gray-500') - - if not item['candidates']: - ui.label('Nenhum resultado encontrado no TMDb.').classes('text-red font-bold') + ui.label(f"Arquivo: {item['original_file']}").classes('text-lg font-bold') + ui.label(f"Guessit: {item['guess_title']} ({item['guess_year']})").classes('text-gray-500 text-sm') with ui.grid(columns=4).classes('w-full gap-4 mt-4'): for cand in item['candidates']: - if item['type'] == 'movie': - title = cand.get('title') - date = cand.get('release_date', '') - img = cand.get('poster_path') - else: - title = cand.get('name') - date = cand.get('first_air_date', '') - img = cand.get('poster_path') - + # Helper para UI + is_movie = (item['type'] == 'movie') + title = cand.get('title') if is_movie else cand.get('name') + date = cand.get('release_date') if is_movie else cand.get('first_air_date') year = date[:4] if date else '????' - img_url = f"https://image.tmdb.org/t/p/w200{img}" if img else 'https://via.placeholder.com/200x300?text=No+Image' + img = cand.get('poster_path') + img_url = f"https://image.tmdb.org/t/p/w200{img}" if img else 'https://via.placeholder.com/200' - with ui.card().classes('cursor-pointer hover:bg-blue-50 p-0 gap-0 border').tight(): + # Previsão da categoria deste candidato + preview_cat = organizer.detect_category(cand, item['type']) + + with ui.card().classes('cursor-pointer hover:bg-blue-50 p-0 gap-0 border relative').tight(): + # Badge de categoria na imagem + ui.label(preview_cat).classes('absolute top-1 right-1 bg-black text-white text-xs px-1 rounded opacity-80') + ui.image(img_url).classes('h-48 w-full object-cover') with ui.column().classes('p-2 w-full'): ui.label(title).classes('font-bold text-sm leading-tight text-ellipsis overflow-hidden') ui.label(year).classes('text-xs text-gray-500') - ui.button('Selecionar', on_click=lambda c=cand: select_match(item, c, row_refresh_callback)).props('sm flat w-full') + ui.button('Escolher', on_click=lambda c=cand: select_match(item, c, row_refresh_callback)).props('sm flat w-full') - ui.button('Fechar', on_click=resolution_dialog.close).props('outline color=red').classes('mt-4 w-full') + ui.button('Cancelar', on_click=resolution_dialog.close).props('outline color=red').classes('mt-4 w-full') resolution_dialog.open() def select_match(item, match, refresh_cb): item['selected_match'] = match item['status'] = 'OK' - organizer.calculate_path(item) + organizer.calculate_path(item) # Recalcula caminho e categoria resolution_dialog.close() refresh_cb() - # --- VIEWS (Mantém igual) --- + # Telas def render_explorer(): main_content.clear() organizer.preview_data = [] with main_content: - # Barra de Navegação + # Caminho atual with ui.row().classes('w-full bg-gray-100 p-2 rounded items-center shadow-sm'): - ui.icon('folder_open', color='grey') + ui.icon('folder', color='grey') ui.label(organizer.path).classes('font-mono ml-2 mr-auto text-sm md:text-base truncate') async def run_analysis(): has_data = await organizer.analyze_folder() if has_data: render_preview() - ui.button('ANALISAR', on_click=run_analysis).props('push color=primary icon=search') + ui.button('ANALISAR PASTA', on_click=run_analysis).props('push color=indigo icon=search') # Lista de Arquivos try: @@ -382,7 +415,7 @@ def create_ui(): ui.item(text='.. (Voltar)', on_click=lambda: navigate(os.path.dirname(organizer.path))).props('clickable icon=arrow_back') for entry in entries: - if entry.name.startswith('.'): continue + if entry.name.startswith('.') or entry.name == "preparados": continue if entry.is_dir(): with ui.item(on_click=lambda p=entry.path: navigate(p)).props('clickable'): @@ -390,76 +423,80 @@ def create_ui(): ui.icon('folder', color='amber') ui.item_section(entry.name) else: - ext = os.path.splitext(entry.name)[1].lower() - is_vid = ext in VIDEO_EXTENSIONS - color = 'blue' if is_vid else 'grey' + is_vid = os.path.splitext(entry.name)[1].lower() in VIDEO_EXTENSIONS icon = 'movie' if is_vid else 'insert_drive_file' - + color = 'blue' if is_vid else 'grey' with ui.item(): with ui.item_section().props('avatar'): ui.icon(icon, color=color) ui.item_section(entry.name).classes('text-sm') except Exception as e: - ui.label(f"Erro ao ler pasta: {e}").classes('text-red') + ui.label(f"Erro: {e}").classes('text-red') def render_preview(): main_content.clear() with main_content: with ui.row().classes('w-full items-center justify-between mb-2'): - ui.label('Revisão').classes('text-xl font-bold') + ui.label('Pré-visualização da Organização').classes('text-xl font-bold') with ui.row(): - ui.button('Cancelar', on_click=render_explorer).props('outline color=red dense') + ui.button('Voltar', on_click=render_explorer).props('outline color=red dense') async def run_move(): if await organizer.execute_move(): render_explorer() - ui.button('MOVER', on_click=run_move).props('push color=green icon=check dense') + ui.button('MOVER ARQUIVOS', on_click=run_move).props('push color=green icon=check dense') - with ui.column().classes('w-full gap-2'): - # Cabeçalho da Lista - with ui.row().classes('w-full bg-gray-200 p-2 font-bold text-sm rounded hidden md:flex'): - ui.label('Original').classes('w-1/3') - ui.label('Destino').classes('w-1/3') - ui.label('Status').classes('w-1/4 text-center') + # Tabela Headers + with ui.row().classes('w-full bg-gray-200 p-2 font-bold text-sm rounded hidden md:flex'): + ui.label('Arquivo Original').classes('w-1/3') + ui.label('Categoria / Destino').classes('w-1/3') + ui.label('Ação').classes('w-1/4 text-center') - with ui.scroll_area().classes('h-[500px] w-full border rounded bg-white'): - def render_row(item): - # Usei refreshable aqui para que o botão atualize apenas a linha - @ui.refreshable - def row_content(): - with ui.row().classes('w-full p-2 border-b items-center hover:bg-gray-50 text-sm'): - with ui.column().classes('w-full md:w-1/3'): - ui.label(item['original_file']).classes('truncate font-medium w-full') - ui.label(f"{item['type'].upper()} • {len(item['subtitles'])} leg.").classes('text-xs text-gray-500') + with ui.scroll_area().classes('h-[600px] w-full border rounded bg-white'): + def render_row(item): + @ui.refreshable + def row_content(): + with ui.row().classes('w-full p-2 border-b items-center hover:bg-gray-50 text-sm'): + # Coluna 1: Origem + with ui.column().classes('w-full md:w-1/3'): + ui.label(item['original_file']).classes('truncate font-medium w-full') + ui.label(f"Guessit: {item['type'].upper()}").classes('text-xs text-gray-500') - with ui.column().classes('w-full md:w-1/3'): - if item['target_path']: - rel_path = os.path.relpath(item['target_path'], ROOT_DIR) - ui.label(rel_path).classes('text-blue-700 font-mono break-all text-xs') - else: - ui.label('---').classes('text-gray-400') - - with ui.row().classes('w-full md:w-1/4 justify-center items-center gap-2'): - status = item['status'] - color = 'green' if status == 'OK' else ('orange' if status == 'CHECK' else 'red') - ui.badge(status, color=color).props('outline') + # Coluna 2: Destino Calculado + with ui.column().classes('w-full md:w-1/3'): + if item['target_path']: + cat = item.get('category', '???') + # Badge da Categoria + colors = {'Animes': 'pink', 'Desenhos': 'orange', 'Filmes': 'blue', 'Séries': 'green'} + cat_color = colors.get(cat, 'grey') - if status != 'OK' and status != 'ERRO_S_E': - ui.button(icon='search', on_click=lambda: open_resolution_dialog(item, row_content.refresh)).props('flat round dense color=primary') - elif status == 'OK': - ui.button(icon='edit', on_click=lambda: open_resolution_dialog(item, row_content.refresh)).props('flat round dense color=grey') - - row_content() + with ui.row().classes('items-center gap-2'): + ui.badge(cat, color=cat_color).props('dense') + rel_path = os.path.relpath(item['target_path'], DEST_DIR) + ui.label(rel_path).classes('font-mono break-all text-xs text-gray-700') + else: + ui.label('--- (Sem destino)').classes('text-gray-400') - for item in organizer.preview_data: - render_row(item) + # Coluna 3: Status/Ação + with ui.row().classes('w-full md:w-1/4 justify-center items-center gap-2'): + status = item['status'] + color = 'green' if status == 'OK' else ('orange' if status == 'CHECK' else 'red') + ui.badge(status, color=color).props('outline') + + btn_icon = 'search' if status != 'OK' else 'edit' + ui.button(icon=btn_icon, on_click=lambda: open_resolution_dialog(item, row_content.refresh)).props('flat round dense color=grey') + + row_content() + + for item in organizer.preview_data: + render_row(item) def navigate(path): organizer.path = path render_explorer() - # Inicia render_explorer() -# Removemos o ui.run() daqui, pois o main.py é quem controla o loop \ No newline at end of file +if __name__ in {"__main__", "__mp_main__"}: + create_ui() diff --git a/data/status.json b/data/status.json index a0085a0..8fa1483 100644 --- a/data/status.json +++ b/data/status.json @@ -1 +1 @@ -{"running": false, "file": "Finalizado \u2705", "pct_file": 100, "pct_total": 100, "log": "Finalizado \u2705"} \ No newline at end of file +{"running": true, "stop_requested": false, "file": "Press\u00e1gio (2009).mkv", "pct_file": 84, "pct_total": 0, "current_index": 1, "total_files": 2, "log": "Velocidade: 9.61x"} \ No newline at end of file