adicionado dowloader do youtube e a opão'deploy' mover o arquivo para diretório final
This commit is contained in:
222
app/modules/deployer.py
Normal file
222
app/modules/deployer.py
Normal 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()
|
||||
Reference in New Issue
Block a user