adicionado dowloader do youtube e a opão'deploy' mover o arquivo para diretório final

This commit is contained in:
2026-01-27 00:28:48 +00:00
parent 56df0b1d1f
commit 8d92610c4c
11 changed files with 569 additions and 76 deletions

222
app/modules/deployer.py Normal file
View File

@@ -0,0 +1,222 @@
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()