from nicegui import ui import os import shutil import datetime # Configurações de Raiz SRC_ROOT = "/downloads" DST_ROOT = "/media" class DeployManager: def __init__(self): self.src_path = SRC_ROOT self.dst_path = DST_ROOT self.selected_items = [] # Lista de caminhos selecionados self.container = None # --- NAVEGAÇÃO --- def navigate_src(self, path): if os.path.exists(path) and os.path.isdir(path): self.src_path = path # Nota: Não limpamos a seleção ao navegar para permitir selecionar coisas de pastas diferentes se quiser # self.selected_items = [] self.refresh() def navigate_dst(self, path): if os.path.exists(path) and os.path.isdir(path): self.dst_path = path self.refresh() def refresh(self): if self.container: self.container.clear() with self.container: self.render_layout() # --- LÓGICA DE SELEÇÃO --- def toggle_selection(self, path): if path in self.selected_items: self.selected_items.remove(path) else: self.selected_items.append(path) # Recarrega para mostrar o checkbox marcado/desmarcado e a cor de fundo self.refresh() # --- AÇÃO DE MOVER --- def execute_move(self): if not self.selected_items: ui.notify('Selecione itens na esquerda para mover.', type='warning') return if self.src_path == self.dst_path: ui.notify('Origem e Destino são iguais!', type='warning') return count = 0 errors = 0 with ui.dialog() as dialog, ui.card(): ui.label('Confirmar Movimentação Definitiva').classes('text-lg font-bold') ui.label(f'Destino: {self.dst_path}') ui.label(f'Itens selecionados: {len(self.selected_items)}') # Lista itens no dialog para conferência with ui.scroll_area().classes('h-32 w-full border p-2 bg-gray-50'): for item in self.selected_items: ui.label(os.path.basename(item)).classes('text-xs') def confirm(): nonlocal count, errors dialog.close() ui.notify('Iniciando movimentação...', type='info') for item_path in self.selected_items: if not os.path.exists(item_path): continue # Já foi movido ou deletado item_name = os.path.basename(item_path) target = os.path.join(self.dst_path, item_name) try: if os.path.exists(target): ui.notify(f'Erro: {item_name} já existe no destino!', type='negative') errors += 1 continue shutil.move(item_path, target) # Tenta ajustar permissões após mover para garantir que o Jellyfin leia try: if os.path.isdir(target): os.system(f'chmod -R 777 "{target}"') else: os.chmod(target, 0o777) except: pass count += 1 except Exception as e: ui.notify(f'Erro ao mover {item_name}: {e}', type='negative') errors += 1 if count > 0: ui.notify(f'{count} itens movidos com sucesso!', type='positive') self.selected_items = [] # Limpa seleção após sucesso self.refresh() with ui.row().classes('w-full justify-end'): ui.button('Cancelar', on_click=dialog.close).props('flat') ui.button('Mover Agora', on_click=confirm).props('color=green icon=move_to_inbox') dialog.open() # --- RENDERIZADORES AUXILIARES --- def render_breadcrumbs(self, current_path, root_dir, nav_callback): with ui.row().classes('items-center gap-1 bg-gray-100 p-1 rounded w-full'): ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm') rel = os.path.relpath(current_path, root_dir) if rel != '.': acc = root_dir parts = rel.split(os.sep) for part in parts: ui.label('/') acc = os.path.join(acc, part) ui.button(part, on_click=lambda p=acc: nav_callback(p)).props('flat dense no-caps size=sm') if current_path != root_dir: ui.space() parent = os.path.dirname(current_path) ui.button(icon='arrow_upward', on_click=lambda: nav_callback(parent)).props('flat round dense size=sm') def render_file_list(self, path, is_source): try: entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower())) except: ui.label('Erro ao ler pasta').classes('text-red') return with ui.scroll_area().classes('h-96 border rounded bg-white'): if not entries: ui.label('Pasta Vazia').classes('p-4 text-gray-400 italic') for entry in entries: is_dir = entry.is_dir() icon = 'folder' if is_dir else 'description' if not is_dir and entry.name.lower().endswith(('.mkv', '.mp4')): icon = 'movie' color = 'amber' if is_dir else 'grey' # Verifica se está selecionado is_selected = entry.path in self.selected_items bg_color = 'bg-blue-100' if is_selected else 'hover:bg-gray-50' # Linha do Arquivo/Pasta with ui.row().classes(f'w-full items-center p-1 cursor-pointer border-b {bg_color}') as row: # Lógica de Clique na Linha (Texto) if is_source: if is_dir: # Se for pasta na origem: Clique entra na pasta row.on('click', lambda p=entry.path: self.navigate_src(p)) else: # Se for arquivo na origem: Clique seleciona row.on('click', lambda p=entry.path: self.toggle_selection(p)) else: # No destino: Clique sempre navega (se for pasta) if is_dir: row.on('click', lambda p=entry.path: self.navigate_dst(p)) # COLUNA 1: Checkbox (Apenas na Origem) if is_source: # O checkbox permite selecionar pastas sem entrar nelas # stop_propagation impede que o clique no checkbox acione o clique da linha (entrar na pasta) ui.checkbox('', value=is_selected, on_change=lambda e, p=entry.path: self.toggle_selection(p)).props('dense').on('click', lambda e: e.stop_propagation()) # COLUNA 2: Ícone ui.icon(icon, color=color).classes('mx-2') # COLUNA 3: Nome ui.label(entry.name).classes('text-sm truncate flex-grow select-none') # --- LAYOUT PRINCIPAL --- def render_layout(self): with ui.row().classes('w-full h-full gap-4'): # ESQUERDA (ORIGEM) with ui.column().classes('w-1/2 h-full'): ui.label('📂 Origem (Downloads)').classes('text-lg font-bold text-blue-600') self.render_breadcrumbs(self.src_path, SRC_ROOT, self.navigate_src) # Contador if self.selected_items: ui.label(f'{len(self.selected_items)} itens selecionados').classes('text-sm font-bold text-blue-800') else: ui.label('Selecione arquivos ou pastas').classes('text-xs text-gray-400') self.render_file_list(self.src_path, is_source=True) # DIREITA (DESTINO) with ui.column().classes('w-1/2 h-full'): ui.label('🏁 Destino (Mídia Final)').classes('text-lg font-bold text-green-600') self.render_breadcrumbs(self.dst_path, DST_ROOT, self.navigate_dst) # Espaçador visual ui.label('Navegue até a pasta de destino').classes('text-xs text-gray-400') self.render_file_list(self.dst_path, is_source=False) # Botão de Ação Principal with ui.row().classes('w-full justify-end mt-4'): ui.button('Mover Selecionados >>>', on_click=self.execute_move)\ .props('icon=arrow_forward color=green')\ .bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0) # --- INICIALIZADOR --- def create_ui(): dm = DeployManager() # Garante pastas for d in [SRC_ROOT, DST_ROOT]: if not os.path.exists(d): try: os.makedirs(d) except: pass dm.container = ui.column().classes('w-full h-full p-4') dm.refresh()