181 lines
7.5 KiB
Python
Executable File
181 lines
7.5 KiB
Python
Executable File
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() |