from nicegui import ui import os import re import shutil ROOT_DIR = "/downloads" # --- UTILITÁRIOS --- def extract_season_episode(filename): """Detecta Temporada e Episódio usando vários padrões""" patterns = [ r'(?i)S(\d{1,4})[\s._-]*E(\d{1,4})', r'(?i)S(\d{1,4})[\s._-]*EP(\d{1,4})', r'(?i)(\d{1,4})x(\d{1,4})', r'(?i)Season[\s._-]*(\d{1,4})[\s._-]*Episode[\s._-]*(\d{1,4})', r'(?i)S(\d{1,4})[\s._-]*-\s*(\d{1,4})', r'(?i)\[(\d{1,4})x(\d{1,4})\]', ] for pattern in patterns: match = re.search(pattern, filename) if match: return match.group(1), match.group(2) return None, None class RenamerManager: def __init__(self): self.path = ROOT_DIR self.container = None self.preview_data = [] self.view_mode = 'explorer' def navigate(self, path): if os.path.exists(path) and os.path.isdir(path): self.path = path self.refresh() else: ui.notify('Erro ao acessar pasta', type='negative') def refresh(self): if self.container: self.container.clear() with self.container: if self.view_mode == 'explorer': self.render_breadcrumbs() self.render_folder_list() else: self.render_preview() def analyze_folder(self): self.preview_data = [] for root, dirs, files in os.walk(self.path): if "finalizados" in root: continue for file in files: if file.lower().endswith(('.mkv', '.mp4', '.avi')): season, episode = extract_season_episode(file) if season and episode: try: s_fmt = f"{int(season):02d}" e_fmt = f"{int(episode):02d}" ext = os.path.splitext(file)[1] # Estrutura: Temporada XX / Episódio YY.mkv new_struct = f"Temporada {s_fmt}/Episódio {e_fmt}{ext}" src = os.path.join(root, file) dst = os.path.join(self.path, f"Temporada {s_fmt}", f"Episódio {e_fmt}{ext}") if src != dst: self.preview_data.append({ 'original': file, 'new': new_struct, 'src': src, 'dst': dst }) except: pass if not self.preview_data: ui.notify('Nenhum padrão encontrado.', type='warning') else: self.view_mode = 'preview' self.refresh() def execute_rename(self): count = 0 for item in self.preview_data: try: os.makedirs(os.path.dirname(item['dst']), exist_ok=True) if not os.path.exists(item['dst']): shutil.move(item['src'], item['dst']) count += 1 except: pass ui.notify(f'{count} Arquivos Organizados!', type='positive') self.view_mode = 'explorer' self.preview_data = [] self.refresh() def cancel(self): self.view_mode = 'explorer' self.preview_data = [] self.refresh() # --- RENDERIZADOR: BARRA DE NAVEGAÇÃO (CADEIA) --- def render_breadcrumbs(self): with ui.row().classes('w-full items-center bg-gray-100 p-2 rounded gap-1'): # Botão Raiz ui.button('🏠', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense text-color=grey-8') # Divide o caminho atual para criar os botões if self.path != ROOT_DIR: rel = os.path.relpath(self.path, ROOT_DIR) parts = rel.split(os.sep) acc = ROOT_DIR for part in parts: ui.icon('chevron_right', color='grey') acc = os.path.join(acc, part) # Botão da Pasta ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps text-color=primary') ui.space() # Botão de Ação Principal ui.button("🔍 Analisar Pasta Atual", on_click=self.analyze_folder).props('push color=primary') # --- RENDERIZADOR: LISTA DE PASTAS --- def render_folder_list(self): try: # Lista apenas diretórios, ignora arquivos entries = sorted([e for e in os.scandir(self.path) if e.is_dir() and not e.name.startswith('.')], key=lambda e: e.name.lower()) except: ui.label("Erro ao ler pasta").classes('text-red') return with ui.column().classes('w-full gap-1 mt-2'): # Botão para subir nível (se não estiver na raiz) if self.path != ROOT_DIR: with ui.item(on_click=lambda: self.navigate(os.path.dirname(self.path))).classes('bg-blue-50 hover:bg-blue-100 cursor-pointer rounded'): with ui.item_section().props('avatar'): ui.icon('arrow_upward', color='grey') with ui.item_section(): ui.item_label('Voltar / Subir Nível') if not entries: ui.label("Nenhuma subpasta aqui.").classes('text-gray-400 italic ml-4 mt-2') # Lista de Subpastas for entry in entries: with ui.item(on_click=lambda p=entry.path: self.navigate(p)).classes('hover:bg-gray-100 cursor-pointer rounded'): with ui.item_section().props('avatar'): ui.icon('folder', color='amber') with ui.item_section(): ui.item_label(entry.name).classes('font-medium') # --- RENDERIZADOR: PREVIEW --- def render_preview(self): with ui.column().classes('w-full items-center gap-4'): ui.label(f'Detectados {len(self.preview_data)} arquivos para renomear').classes('text-xl font-bold text-green-700') with ui.row(): ui.button('Cancelar', on_click=self.cancel).props('outline color=red') ui.button('Confirmar Tudo', on_click=self.execute_rename).props('push color=green icon=check') # Tabela Simples with ui.card().classes('w-full p-0'): with ui.column().classes('w-full gap-0'): # Cabeçalho with ui.row().classes('w-full bg-gray-200 p-2 font-bold'): ui.label('Original').classes('w-1/2') ui.label('Novo Caminho').classes('w-1/2') # Itens with ui.scroll_area().classes('h-96 w-full'): for item in self.preview_data: with ui.row().classes('w-full p-2 border-b border-gray-100 hover:bg-gray-50'): ui.label(item['original']).classes('w-1/2 text-sm truncate') ui.label(item['new']).classes('w-1/2 text-sm text-blue-600 font-mono truncate') # --- INICIALIZADOR --- def create_ui(): rm = RenamerManager() rm.container = ui.column().classes('w-full h-full p-4 gap-4') rm.refresh()