adicionado dowloader do youtube e a opão'deploy' mover o arquivo para diretório final
This commit is contained in:
30
app/main.py
30
app/main.py
@@ -1,33 +1,33 @@
|
|||||||
from nicegui import ui, app
|
from nicegui import ui, app
|
||||||
from modules import file_manager, renamer, encoder
|
# ADICIONE 'downloader' AQUI:
|
||||||
|
from modules import file_manager, renamer, encoder, downloader, deployer
|
||||||
|
app.add_static_files('/files', '/downloads')
|
||||||
|
|
||||||
# Configuração Geral
|
# ATUALIZE AS ABAS:
|
||||||
ui.colors(primary='#5898d4', secondary='#26a69a', accent='#9c27b0', positive='#21ba45')
|
|
||||||
|
|
||||||
# Cabeçalho
|
|
||||||
with ui.header().classes('items-center justify-between'):
|
|
||||||
ui.label('🎬 PyMedia Manager').classes('text-2xl font-bold')
|
|
||||||
ui.button('Sair', on_click=app.shutdown, icon='logout').props('flat color=white')
|
|
||||||
|
|
||||||
# Abas
|
|
||||||
with ui.tabs().classes('w-full') as tabs:
|
with ui.tabs().classes('w-full') as tabs:
|
||||||
t_files = ui.tab('Gerenciador', icon='folder')
|
t_files = ui.tab('Gerenciador', icon='folder')
|
||||||
t_rename = ui.tab('Renomeador', icon='edit')
|
t_rename = ui.tab('Renomeador', icon='edit')
|
||||||
t_encode = ui.tab('Encoder', icon='movie')
|
t_encode = ui.tab('Encoder', icon='movie')
|
||||||
|
t_down = ui.tab('Downloader', icon='download') # NOVA ABA
|
||||||
|
t_deploy = ui.tab('Mover Final', icon='publish') # NOVA ABA
|
||||||
|
|
||||||
# Painéis
|
# ATUALIZE OS PAINÉIS:
|
||||||
with ui.tab_panels(tabs, value=t_files).classes('w-full p-0'):
|
with ui.tab_panels(tabs, value=t_files).classes('w-full p-0'):
|
||||||
|
|
||||||
# PAINEL 1: FILE MANAGER
|
|
||||||
with ui.tab_panel(t_files).classes('p-0'):
|
with ui.tab_panel(t_files).classes('p-0'):
|
||||||
file_manager.create_ui()
|
file_manager.create_ui()
|
||||||
|
|
||||||
# PAINEL 2: RENAMER
|
|
||||||
with ui.tab_panel(t_rename):
|
with ui.tab_panel(t_rename):
|
||||||
renamer.create_ui()
|
renamer.create_ui()
|
||||||
|
|
||||||
# PAINEL 3: ENCODER
|
|
||||||
with ui.tab_panel(t_encode):
|
with ui.tab_panel(t_encode):
|
||||||
encoder.create_ui()
|
encoder.create_ui()
|
||||||
|
|
||||||
|
# NOVO PAINEL:
|
||||||
|
with ui.tab_panel(t_down):
|
||||||
|
downloader.create_ui()
|
||||||
|
|
||||||
ui.run(title='PyMedia Manager', port=8080, reload=True, storage_secret='secret')
|
with ui.tab_panel(t_deploy):
|
||||||
|
deployer.create_ui()
|
||||||
|
|
||||||
|
ui.run(title='PyMedia Manager', port=8080, reload=True, storage_secret='secret')
|
||||||
BIN
app/modules/__pycache__/deployer.cpython-310.pyc
Normal file
BIN
app/modules/__pycache__/deployer.cpython-310.pyc
Normal file
Binary file not shown.
BIN
app/modules/__pycache__/downloader.cpython-310.pyc
Normal file
BIN
app/modules/__pycache__/downloader.cpython-310.pyc
Normal file
Binary file not shown.
Binary file not shown.
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()
|
||||||
186
app/modules/downloader.py
Normal file
186
app/modules/downloader.py
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
from nicegui import ui
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import yt_dlp
|
||||||
|
|
||||||
|
# --- CONFIGURAÇÕES ---
|
||||||
|
DOWNLOAD_DIR = "/downloads/Youtube"
|
||||||
|
STATUS_FILE = "/app/data/dl_status.json"
|
||||||
|
|
||||||
|
# --- UTILITÁRIOS ---
|
||||||
|
def save_status(data):
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'w') as f: json.dump(data, f)
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
def load_status():
|
||||||
|
if not os.path.exists(STATUS_FILE): return None
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f: return json.load(f)
|
||||||
|
except: return None
|
||||||
|
|
||||||
|
# --- WORKER (BACKEND) ---
|
||||||
|
class DownloadWorker(threading.Thread):
|
||||||
|
def __init__(self, url, format_type):
|
||||||
|
super().__init__()
|
||||||
|
self.url = url
|
||||||
|
self.format_type = format_type
|
||||||
|
self.daemon = True
|
||||||
|
self.stop_requested = False
|
||||||
|
|
||||||
|
def progress_hook(self, d):
|
||||||
|
if self.stop_requested:
|
||||||
|
raise Exception("Cancelado pelo usuário")
|
||||||
|
|
||||||
|
if d['status'] == 'downloading':
|
||||||
|
total = d.get('total_bytes') or d.get('total_bytes_estimate') or 0
|
||||||
|
downloaded = d.get('downloaded_bytes', 0)
|
||||||
|
pct = int((downloaded / total) * 100) if total > 0 else 0
|
||||||
|
|
||||||
|
speed = d.get('speed', 0) or 0
|
||||||
|
speed_str = f"{speed / 1024 / 1024:.2f} MiB/s"
|
||||||
|
filename = os.path.basename(d.get('filename', 'Baixando...'))
|
||||||
|
|
||||||
|
save_status({
|
||||||
|
"running": True,
|
||||||
|
"file": filename,
|
||||||
|
"progress": pct,
|
||||||
|
"log": f"Baixando: {speed_str} | {d.get('_eta_str', '?')} restantes",
|
||||||
|
"stop_requested": False
|
||||||
|
})
|
||||||
|
|
||||||
|
elif d['status'] == 'finished':
|
||||||
|
save_status({
|
||||||
|
"running": True,
|
||||||
|
"file": "Processando...",
|
||||||
|
"progress": 99,
|
||||||
|
"log": "Convertendo/Juntando arquivos...",
|
||||||
|
"stop_requested": False
|
||||||
|
})
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
if not os.path.exists(DOWNLOAD_DIR): os.makedirs(DOWNLOAD_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
ydl_opts = {
|
||||||
|
'outtmpl': f'{DOWNLOAD_DIR}/%(title)s.%(ext)s',
|
||||||
|
'progress_hooks': [self.progress_hook],
|
||||||
|
'nocheckcertificate': True,
|
||||||
|
'ignoreerrors': True,
|
||||||
|
'ffmpeg_location': '/usr/bin/ffmpeg'
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.format_type == 'best':
|
||||||
|
ydl_opts['format'] = 'bestvideo+bestaudio/best'
|
||||||
|
ydl_opts['merge_output_format'] = 'mkv'
|
||||||
|
elif self.format_type == 'audio':
|
||||||
|
ydl_opts['format'] = 'bestaudio/best'
|
||||||
|
ydl_opts['postprocessors'] = [{'key': 'FFmpegExtractAudio','preferredcodec': 'mp3','preferredquality': '192'}]
|
||||||
|
elif self.format_type == '1080p':
|
||||||
|
ydl_opts['format'] = 'bestvideo[height<=1080]+bestaudio/best[height<=1080]'
|
||||||
|
ydl_opts['merge_output_format'] = 'mkv'
|
||||||
|
|
||||||
|
try:
|
||||||
|
save_status({"running": True, "file": "Iniciando...", "progress": 0, "log": "Conectando..."})
|
||||||
|
with yt_dlp.YoutubeDL(ydl_opts) as ydl:
|
||||||
|
ydl.download([self.url])
|
||||||
|
save_status({"running": False, "file": "Concluído!", "progress": 100, "log": "Sucesso."})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
msg = "Cancelado." if "Cancelado" in str(e) else str(e)
|
||||||
|
save_status({"running": False, "file": "Parado", "progress": 0, "log": msg})
|
||||||
|
|
||||||
|
# --- INTERFACE (FRONTEND) ---
|
||||||
|
class DownloaderInterface:
|
||||||
|
def __init__(self):
|
||||||
|
self.container = None
|
||||||
|
self.timer = None
|
||||||
|
self.btn_download = None
|
||||||
|
self.card_status = None
|
||||||
|
|
||||||
|
# Elementos dinâmicos
|
||||||
|
self.lbl_file = None
|
||||||
|
self.progress = None
|
||||||
|
self.lbl_log = None
|
||||||
|
self.btn_stop = None
|
||||||
|
|
||||||
|
def start_download(self, url, fmt):
|
||||||
|
if not url:
|
||||||
|
ui.notify('Cole uma URL!', type='warning')
|
||||||
|
return
|
||||||
|
|
||||||
|
if os.path.exists(STATUS_FILE): os.remove(STATUS_FILE)
|
||||||
|
t = DownloadWorker(url, fmt)
|
||||||
|
t.start()
|
||||||
|
ui.notify('Iniciando...')
|
||||||
|
self.render_update()
|
||||||
|
|
||||||
|
def stop_download(self):
|
||||||
|
data = load_status()
|
||||||
|
if data:
|
||||||
|
data['stop_requested'] = True
|
||||||
|
save_status(data)
|
||||||
|
ui.notify('Parando...')
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
ui.label('📺 YouTube Downloader').classes('text-xl font-bold mb-2')
|
||||||
|
|
||||||
|
# --- INPUT ---
|
||||||
|
with ui.card().classes('w-full p-4 mb-4'):
|
||||||
|
url_input = ui.input('URL do Vídeo').classes('w-full').props('clearable placeholder="https://youtube.com/..."')
|
||||||
|
|
||||||
|
with ui.row().classes('items-center mt-2'):
|
||||||
|
fmt_select = ui.select(
|
||||||
|
{'best': 'Melhor Qualidade (MKV)', '1080p': 'Limitado a 1080p (MKV)', 'audio': 'Apenas Áudio (MP3)'},
|
||||||
|
value='best', label='Formato'
|
||||||
|
).classes('w-64')
|
||||||
|
|
||||||
|
self.btn_download = ui.button('Baixar', on_click=lambda: self.start_download(url_input.value, fmt_select.value))\
|
||||||
|
.props('icon=download color=primary')
|
||||||
|
|
||||||
|
# --- MONITORAMENTO ---
|
||||||
|
# CORREÇÃO AQUI: Criamos o card primeiro, depois definimos visibilidade
|
||||||
|
self.card_status = ui.card().classes('w-full p-4')
|
||||||
|
self.card_status.visible = False # Esconde inicialmente
|
||||||
|
|
||||||
|
with self.card_status:
|
||||||
|
ui.label('Progresso').classes('font-bold')
|
||||||
|
self.lbl_file = ui.label('Aguardando...')
|
||||||
|
self.progress = ui.linear_progress(value=0).classes('w-full')
|
||||||
|
self.lbl_log = ui.label('---').classes('text-sm text-gray-500 font-mono')
|
||||||
|
|
||||||
|
with ui.row().classes('w-full justify-end mt-2'):
|
||||||
|
self.btn_stop = ui.button('🛑 Cancelar', on_click=self.stop_download).props('color=red flat')
|
||||||
|
|
||||||
|
self.timer = ui.timer(1.0, self.render_update)
|
||||||
|
|
||||||
|
def render_update(self):
|
||||||
|
data = load_status()
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
if self.card_status: self.card_status.visible = False
|
||||||
|
if self.btn_download: self.btn_download.enable()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Atualiza UI
|
||||||
|
is_running = data.get('running', False)
|
||||||
|
|
||||||
|
if self.btn_download:
|
||||||
|
if is_running: self.btn_download.disable()
|
||||||
|
else: self.btn_download.enable()
|
||||||
|
|
||||||
|
if self.card_status: self.card_status.visible = True
|
||||||
|
|
||||||
|
if self.lbl_file: self.lbl_file.text = f"Arquivo: {data.get('file', '?')}"
|
||||||
|
if self.progress: self.progress.value = data.get('progress', 0) / 100
|
||||||
|
if self.lbl_log: self.lbl_log.text = data.get('log', '')
|
||||||
|
|
||||||
|
if self.btn_stop: self.btn_stop.visible = is_running
|
||||||
|
|
||||||
|
# --- INICIALIZADOR ---
|
||||||
|
def create_ui():
|
||||||
|
dl = DownloaderInterface()
|
||||||
|
dl.container = ui.column().classes('w-full h-full p-4 gap-4')
|
||||||
|
with dl.container:
|
||||||
|
dl.render()
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
from nicegui import ui
|
from nicegui import ui, app
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
import datetime
|
import datetime
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
ROOT_DIR = "/downloads"
|
ROOT_DIR = "/downloads"
|
||||||
|
|
||||||
@@ -22,6 +24,45 @@ def get_subfolders(root):
|
|||||||
except: pass
|
except: pass
|
||||||
return sorted(folders)
|
return sorted(folders)
|
||||||
|
|
||||||
|
# --- LEITOR DE METADADOS (FFPROBE) ---
|
||||||
|
def get_media_info(filepath):
|
||||||
|
"""Lê as faixas de áudio e legenda do arquivo"""
|
||||||
|
cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", filepath]
|
||||||
|
try:
|
||||||
|
res = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
data = json.loads(res.stdout)
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"duration": float(data['format'].get('duration', 0)),
|
||||||
|
"bitrate": int(data['format'].get('bit_rate', 0)),
|
||||||
|
"video": [],
|
||||||
|
"audio": [],
|
||||||
|
"subtitle": []
|
||||||
|
}
|
||||||
|
|
||||||
|
for s in data.get('streams', []):
|
||||||
|
type = s['codec_type']
|
||||||
|
lang = s.get('tags', {}).get('language', 'und')
|
||||||
|
title = s.get('tags', {}).get('title', '')
|
||||||
|
codec = s.get('codec_name', 'unknown')
|
||||||
|
|
||||||
|
desc = f"[{lang.upper()}] {codec}"
|
||||||
|
if title: desc += f" - {title}"
|
||||||
|
|
||||||
|
if type == 'video':
|
||||||
|
w = s.get('width', 0)
|
||||||
|
h = s.get('height', 0)
|
||||||
|
info['video'].append(f"{codec.upper()} ({w}x{h})")
|
||||||
|
elif type == 'audio':
|
||||||
|
ch = s.get('channels', 0)
|
||||||
|
info['audio'].append(f"{desc} ({ch}ch)")
|
||||||
|
elif type == 'subtitle':
|
||||||
|
info['subtitle'].append(desc)
|
||||||
|
|
||||||
|
return info
|
||||||
|
except:
|
||||||
|
return None
|
||||||
|
|
||||||
# --- CLASSE GERENCIADORA ---
|
# --- CLASSE GERENCIADORA ---
|
||||||
class FileManager:
|
class FileManager:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@@ -52,10 +93,69 @@ class FileManager:
|
|||||||
self.render_header()
|
self.render_header()
|
||||||
self.render_content()
|
self.render_content()
|
||||||
|
|
||||||
# --- DIÁLOGOS DE AÇÃO (ORDEM CORRIGIDA) ---
|
# --- PLAYER DE VÍDEO ---
|
||||||
|
def open_player(self, path):
|
||||||
|
filename = os.path.basename(path)
|
||||||
|
|
||||||
|
# Converte caminho local (/downloads/pasta/video.mkv) para URL (/files/pasta/video.mkv)
|
||||||
|
# O prefixo /files foi configurado no main.py
|
||||||
|
rel_path = os.path.relpath(path, ROOT_DIR)
|
||||||
|
video_url = f"/files/{rel_path}"
|
||||||
|
|
||||||
|
# Pega dados técnicos
|
||||||
|
info = get_media_info(path)
|
||||||
|
|
||||||
|
with ui.dialog() as dialog, ui.card().classes('w-full max-w-4xl h-[80vh] p-0 gap-0'):
|
||||||
|
# Header
|
||||||
|
with ui.row().classes('w-full bg-gray-100 p-2 justify-between items-center'):
|
||||||
|
ui.label(filename).classes('font-bold text-lg truncate')
|
||||||
|
ui.button(icon='close', on_click=dialog.close).props('flat round dense')
|
||||||
|
|
||||||
|
with ui.row().classes('w-full h-full'):
|
||||||
|
# Coluna Esquerda: Player
|
||||||
|
with ui.column().classes('w-2/3 h-full bg-black justify-center'):
|
||||||
|
# Player HTML5 Nativo
|
||||||
|
ui.video(video_url).classes('w-full max-h-full')
|
||||||
|
ui.label('Nota: Áudios AC3/DTS podem ficar mudos no navegador.').classes('text-gray-500 text-xs text-center w-full')
|
||||||
|
|
||||||
|
# Coluna Direita: Informações
|
||||||
|
with ui.column().classes('w-1/3 h-full p-4 overflow-y-auto bg-white border-l'):
|
||||||
|
ui.label('📋 Detalhes do Arquivo').classes('text-lg font-bold mb-4 text-blue-600')
|
||||||
|
|
||||||
|
if info:
|
||||||
|
# Vídeo
|
||||||
|
ui.label('Vídeo').classes('font-bold text-xs text-gray-500 uppercase')
|
||||||
|
for v in info['video']:
|
||||||
|
ui.label(f"📺 {v}").classes('ml-2 text-sm')
|
||||||
|
|
||||||
|
ui.separator().classes('my-2')
|
||||||
|
|
||||||
|
# Áudio
|
||||||
|
ui.label('Áudio').classes('font-bold text-xs text-gray-500 uppercase')
|
||||||
|
if info['audio']:
|
||||||
|
for a in info['audio']:
|
||||||
|
ui.label(f"🔊 {a}").classes('ml-2 text-sm')
|
||||||
|
else:
|
||||||
|
ui.label("Sem áudio").classes('ml-2 text-sm text-gray-400')
|
||||||
|
|
||||||
|
ui.separator().classes('my-2')
|
||||||
|
|
||||||
|
# Legenda
|
||||||
|
ui.label('Legendas').classes('font-bold text-xs text-gray-500 uppercase')
|
||||||
|
if info['subtitle']:
|
||||||
|
for s in info['subtitle']:
|
||||||
|
ui.label(f"💬 {s}").classes('ml-2 text-sm')
|
||||||
|
else:
|
||||||
|
ui.label("Sem legendas").classes('ml-2 text-sm text-gray-400')
|
||||||
|
else:
|
||||||
|
ui.label('Não foi possível ler os metadados.').classes('text-red')
|
||||||
|
|
||||||
|
dialog.open()
|
||||||
|
|
||||||
|
# --- DIÁLOGOS DE AÇÃO ---
|
||||||
def open_delete_dialog(self, path):
|
def open_delete_dialog(self, path):
|
||||||
with ui.dialog() as dialog, ui.card():
|
with ui.dialog() as dialog, ui.card():
|
||||||
ui.label('Excluir item permanentemente?').classes('text-lg font-bold')
|
ui.label('Excluir item?').classes('font-bold')
|
||||||
ui.label(os.path.basename(path))
|
ui.label(os.path.basename(path))
|
||||||
with ui.row().classes('w-full justify-end'):
|
with ui.row().classes('w-full justify-end'):
|
||||||
ui.button('Cancelar', on_click=dialog.close).props('flat')
|
ui.button('Cancelar', on_click=dialog.close).props('flat')
|
||||||
@@ -63,47 +163,42 @@ class FileManager:
|
|||||||
try:
|
try:
|
||||||
if os.path.isdir(path): shutil.rmtree(path)
|
if os.path.isdir(path): shutil.rmtree(path)
|
||||||
else: os.remove(path)
|
else: os.remove(path)
|
||||||
# 1. Notifica e Fecha ANTES de destruir a UI
|
|
||||||
ui.notify('Excluído!', type='positive')
|
|
||||||
dialog.close()
|
dialog.close()
|
||||||
# 2. Atualiza a tela (Destrói elementos antigos)
|
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
ui.notify('Excluído!')
|
||||||
except Exception as e: ui.notify(str(e), type='negative')
|
except Exception as e: ui.notify(str(e), type='negative')
|
||||||
ui.button('Excluir', on_click=confirm).props('color=red')
|
ui.button('Excluir', on_click=confirm).props('color=red')
|
||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
def open_rename_dialog(self, path):
|
def open_rename_dialog(self, path):
|
||||||
with ui.dialog() as dialog, ui.card():
|
with ui.dialog() as dialog, ui.card():
|
||||||
ui.label('Renomear').classes('text-lg')
|
ui.label('Renomear')
|
||||||
name_input = ui.input('Novo Nome', value=os.path.basename(path)).classes('w-full')
|
name = ui.input('Novo Nome', value=os.path.basename(path)).classes('w-full')
|
||||||
def save():
|
def save():
|
||||||
try:
|
try:
|
||||||
new_path = os.path.join(os.path.dirname(path), name_input.value)
|
os.rename(path, os.path.join(os.path.dirname(path), name.value))
|
||||||
os.rename(path, new_path)
|
|
||||||
ui.notify('Renomeado!', type='positive')
|
|
||||||
dialog.close()
|
dialog.close()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
ui.notify('Renomeado!')
|
||||||
except Exception as e: ui.notify(str(e), type='negative')
|
except Exception as e: ui.notify(str(e), type='negative')
|
||||||
ui.button('Salvar', on_click=save).props('color=primary')
|
ui.button('Salvar', on_click=save)
|
||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
def open_move_dialog(self, path):
|
def open_move_dialog(self, path):
|
||||||
folders = get_subfolders(ROOT_DIR)
|
folders = get_subfolders(ROOT_DIR)
|
||||||
if os.path.isdir(path) and path in folders: folders.remove(path)
|
if os.path.isdir(path) and path in folders: folders.remove(path)
|
||||||
|
|
||||||
opts = {f: f.replace(ROOT_DIR, "Raiz") if f != ROOT_DIR else "Raiz" for f in folders}
|
opts = {f: f.replace(ROOT_DIR, "Raiz") if f != ROOT_DIR else "Raiz" for f in folders}
|
||||||
|
|
||||||
with ui.dialog() as dialog, ui.card().classes('w-96'):
|
with ui.dialog() as dialog, ui.card().classes('w-96'):
|
||||||
ui.label('Mover Para...').classes('text-lg')
|
ui.label('Mover Para')
|
||||||
target = ui.select(opts, value=ROOT_DIR, with_input=True).classes('w-full')
|
target = ui.select(opts, value=ROOT_DIR, with_input=True).classes('w-full')
|
||||||
def confirm():
|
def confirm():
|
||||||
try:
|
try:
|
||||||
shutil.move(path, target.value)
|
shutil.move(path, target.value)
|
||||||
ui.notify('Movido!', type='positive')
|
|
||||||
dialog.close()
|
dialog.close()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
|
ui.notify('Movido!')
|
||||||
except Exception as e: ui.notify(str(e), type='negative')
|
except Exception as e: ui.notify(str(e), type='negative')
|
||||||
ui.button('Mover', on_click=confirm).props('color=primary')
|
ui.button('Mover', on_click=confirm)
|
||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
def open_create_folder(self):
|
def open_create_folder(self):
|
||||||
@@ -113,25 +208,24 @@ class FileManager:
|
|||||||
def create():
|
def create():
|
||||||
try:
|
try:
|
||||||
os.makedirs(os.path.join(self.path, name.value))
|
os.makedirs(os.path.join(self.path, name.value))
|
||||||
ui.notify('Pasta criada!', type='positive')
|
|
||||||
dialog.close()
|
dialog.close()
|
||||||
self.refresh()
|
self.refresh()
|
||||||
except Exception as e: ui.notify(str(e), type='negative')
|
except Exception as e: ui.notify(str(e), type='negative')
|
||||||
ui.button('Criar', on_click=create)
|
ui.button('Criar', on_click=create)
|
||||||
dialog.open()
|
dialog.open()
|
||||||
|
|
||||||
# --- MENU DE CONTEXTO (CORRIGIDO) ---
|
# --- MENU DE CONTEXTO ---
|
||||||
def bind_context_menu(self, element, entry):
|
def bind_context_menu(self, element, entry):
|
||||||
"""
|
|
||||||
CORREÇÃO: Usa 'contextmenu.prevent' para bloquear o menu do navegador.
|
|
||||||
"""
|
|
||||||
with ui.menu() as m:
|
with ui.menu() as m:
|
||||||
|
if not entry.is_dir and entry.name.lower().endswith(('.mkv', '.mp4', '.avi')):
|
||||||
|
ui.menu_item('▶️ Reproduzir / Detalhes', on_click=lambda: self.open_player(entry.path))
|
||||||
|
ui.separator()
|
||||||
|
|
||||||
ui.menu_item('Renomear', on_click=lambda: self.open_rename_dialog(entry.path))
|
ui.menu_item('Renomear', on_click=lambda: self.open_rename_dialog(entry.path))
|
||||||
ui.menu_item('Mover Para...', on_click=lambda: self.open_move_dialog(entry.path))
|
ui.menu_item('Mover Para...', on_click=lambda: self.open_move_dialog(entry.path))
|
||||||
ui.separator()
|
ui.separator()
|
||||||
ui.menu_item('Excluir', on_click=lambda: self.open_delete_dialog(entry.path)).props('text-color=red')
|
ui.menu_item('Excluir', on_click=lambda: self.open_delete_dialog(entry.path)).props('text-color=red')
|
||||||
|
|
||||||
# Sintaxe correta do NiceGUI para prevenir default (Botão direito nativo)
|
|
||||||
element.on('contextmenu.prevent', lambda: m.open())
|
element.on('contextmenu.prevent', lambda: m.open())
|
||||||
|
|
||||||
# --- RENDERIZADORES ---
|
# --- RENDERIZADORES ---
|
||||||
@@ -142,10 +236,8 @@ class FileManager:
|
|||||||
else:
|
else:
|
||||||
ui.button(icon='home').props('flat round dense disabled text-color=grey')
|
ui.button(icon='home').props('flat round dense disabled text-color=grey')
|
||||||
|
|
||||||
# Breadcrumbs
|
|
||||||
rel = os.path.relpath(self.path, ROOT_DIR)
|
rel = os.path.relpath(self.path, ROOT_DIR)
|
||||||
parts = rel.split(os.sep) if rel != '.' else []
|
parts = rel.split(os.sep) if rel != '.' else []
|
||||||
|
|
||||||
with ui.row().classes('items-center gap-0'):
|
with ui.row().classes('items-center gap-0'):
|
||||||
ui.button('/', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense no-caps min-w-0 px-2')
|
ui.button('/', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense no-caps min-w-0 px-2')
|
||||||
acc = ROOT_DIR
|
acc = ROOT_DIR
|
||||||
@@ -155,25 +247,19 @@ class FileManager:
|
|||||||
ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps min-w-0 px-2')
|
ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps min-w-0 px-2')
|
||||||
|
|
||||||
ui.space()
|
ui.space()
|
||||||
ui.button(icon='create_new_folder', on_click=self.open_create_folder).props('flat round dense').tooltip('Nova Pasta')
|
ui.button(icon='create_new_folder', on_click=self.open_create_folder).props('flat round dense')
|
||||||
|
ui.button(icon='view_list' if self.view_mode == 'grid' else 'grid_view', on_click=self.toggle_view).props('flat round dense')
|
||||||
icon_view = 'view_list' if self.view_mode == 'grid' else 'grid_view'
|
|
||||||
ui.button(icon=icon_view, on_click=self.toggle_view).props('flat round dense').tooltip('Mudar Visualização')
|
|
||||||
|
|
||||||
ui.button(icon='refresh', on_click=self.refresh).props('flat round dense')
|
ui.button(icon='refresh', on_click=self.refresh).props('flat round dense')
|
||||||
|
|
||||||
def render_content(self):
|
def render_content(self):
|
||||||
try:
|
try:
|
||||||
entries = sorted(os.scandir(self.path), key=lambda e: (not e.is_dir(), e.name.lower()))
|
entries = sorted(os.scandir(self.path), key=lambda e: (not e.is_dir(), e.name.lower()))
|
||||||
except:
|
except: return
|
||||||
ui.label('Erro ao ler pasta').classes('text-red')
|
|
||||||
return
|
|
||||||
|
|
||||||
if not entries:
|
if not entries:
|
||||||
ui.label('Pasta vazia').classes('w-full text-center text-gray-400 mt-10')
|
ui.label('Pasta vazia').classes('w-full text-center text-gray-400 mt-10')
|
||||||
return
|
return
|
||||||
|
|
||||||
# === GRID ===
|
|
||||||
if self.view_mode == 'grid':
|
if self.view_mode == 'grid':
|
||||||
with ui.grid().classes('w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3'):
|
with ui.grid().classes('w-full grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 gap-3'):
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
@@ -184,33 +270,30 @@ class FileManager:
|
|||||||
if icon == 'movie': color = 'purple-6'
|
if icon == 'movie': color = 'purple-6'
|
||||||
|
|
||||||
with ui.card().classes('w-full aspect-square p-2 items-center justify-center relative group hover:shadow-md cursor-pointer select-none') as card:
|
with ui.card().classes('w-full aspect-square p-2 items-center justify-center relative group hover:shadow-md cursor-pointer select-none') as card:
|
||||||
if is_dir: card.on('click', lambda p=entry.path: self.navigate(p))
|
if is_dir:
|
||||||
|
card.on('click', lambda p=entry.path: self.navigate(p))
|
||||||
|
elif icon == 'movie':
|
||||||
|
# Duplo clique no vídeo abre o player
|
||||||
|
card.on('dblclick', lambda p=entry.path: self.open_player(p))
|
||||||
|
|
||||||
# CORREÇÃO: Bind correto do menu de contexto
|
|
||||||
self.bind_context_menu(card, entry)
|
self.bind_context_menu(card, entry)
|
||||||
|
|
||||||
ui.icon(icon, size='3rem', color=color)
|
ui.icon(icon, size='3rem', color=color)
|
||||||
ui.label(entry.name).classes('text-xs text-center leading-tight line-clamp-2 w-full break-all')
|
ui.label(entry.name).classes('text-xs text-center leading-tight line-clamp-2 w-full break-all')
|
||||||
|
if not is_dir: ui.label(get_human_size(entry.stat().st_size)).classes('text-[10px] text-gray-400')
|
||||||
if not is_dir:
|
|
||||||
ui.label(get_human_size(entry.stat().st_size)).classes('text-[10px] text-gray-400')
|
|
||||||
|
|
||||||
with ui.button(icon='more_vert').props('flat round dense size=sm').classes('absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity bg-white/90'):
|
with ui.button(icon='more_vert').props('flat round dense size=sm').classes('absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity bg-white/90'):
|
||||||
with ui.menu():
|
with ui.menu():
|
||||||
|
if icon == 'movie':
|
||||||
|
ui.menu_item('▶️ Play', on_click=lambda p=entry.path: self.open_player(p))
|
||||||
|
ui.separator()
|
||||||
ui.menu_item('Renomear', on_click=lambda p=entry.path: self.open_rename_dialog(p))
|
ui.menu_item('Renomear', on_click=lambda p=entry.path: self.open_rename_dialog(p))
|
||||||
ui.menu_item('Mover Para...', on_click=lambda p=entry.path: self.open_move_dialog(p))
|
ui.menu_item('Mover', on_click=lambda p=entry.path: self.open_move_dialog(p))
|
||||||
ui.separator()
|
ui.separator()
|
||||||
ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p)).props('text-color=red')
|
ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p)).props('text-color=red')
|
||||||
|
|
||||||
# === LIST ===
|
|
||||||
else:
|
else:
|
||||||
with ui.column().classes('w-full gap-0'):
|
with ui.column().classes('w-full gap-0'):
|
||||||
with ui.row().classes('w-full px-2 py-1 bg-gray-100 text-xs font-bold text-gray-500 hidden sm:flex'):
|
|
||||||
ui.label('Nome').classes('flex-grow')
|
|
||||||
ui.label('Tamanho').classes('w-24 text-right')
|
|
||||||
ui.label('Data').classes('w-32 text-right')
|
|
||||||
ui.label('').classes('w-8')
|
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
is_dir = entry.is_dir()
|
is_dir = entry.is_dir()
|
||||||
icon = 'folder' if is_dir else 'description'
|
icon = 'folder' if is_dir else 'description'
|
||||||
@@ -218,30 +301,26 @@ class FileManager:
|
|||||||
|
|
||||||
with ui.row().classes('w-full items-center px-2 py-2 border-b hover:bg-blue-50 cursor-pointer group') as row:
|
with ui.row().classes('w-full items-center px-2 py-2 border-b hover:bg-blue-50 cursor-pointer group') as row:
|
||||||
if is_dir: row.on('click', lambda p=entry.path: self.navigate(p))
|
if is_dir: row.on('click', lambda p=entry.path: self.navigate(p))
|
||||||
|
elif entry.name.lower().endswith(('.mkv', '.mp4')):
|
||||||
|
row.on('dblclick', lambda p=entry.path: self.open_player(p))
|
||||||
|
|
||||||
self.bind_context_menu(row, entry)
|
self.bind_context_menu(row, entry)
|
||||||
|
|
||||||
ui.icon(icon, color=color).classes('mr-2')
|
ui.icon(icon, color=color).classes('mr-2')
|
||||||
|
|
||||||
with ui.column().classes('flex-grow gap-0'):
|
with ui.column().classes('flex-grow gap-0'):
|
||||||
ui.label(entry.name).classes('text-sm font-medium break-all')
|
ui.label(entry.name).classes('text-sm font-medium break-all')
|
||||||
if not is_dir:
|
|
||||||
ui.label(get_human_size(entry.stat().st_size)).classes('text-[10px] text-gray-400 sm:hidden')
|
if not is_dir:
|
||||||
|
ui.label(get_human_size(entry.stat().st_size)).classes('text-xs text-gray-500 mr-4')
|
||||||
sz = "-" if is_dir else get_human_size(entry.stat().st_size)
|
|
||||||
ui.label(sz).classes('w-24 text-right text-xs text-gray-500 hidden sm:block')
|
|
||||||
|
|
||||||
dt = datetime.datetime.fromtimestamp(entry.stat().st_mtime).strftime('%d/%m/%Y')
|
|
||||||
ui.label(dt).classes('w-32 text-right text-xs text-gray-500 hidden sm:block')
|
|
||||||
|
|
||||||
with ui.button(icon='more_vert').props('flat round dense size=sm').classes('sm:opacity-0 group-hover:opacity-100'):
|
with ui.button(icon='more_vert').props('flat round dense size=sm').classes('sm:opacity-0 group-hover:opacity-100'):
|
||||||
with ui.menu():
|
with ui.menu():
|
||||||
|
if not is_dir:
|
||||||
|
ui.menu_item('▶️ Play', on_click=lambda p=entry.path: self.open_player(p))
|
||||||
ui.menu_item('Renomear', on_click=lambda p=entry.path: self.open_rename_dialog(p))
|
ui.menu_item('Renomear', on_click=lambda p=entry.path: self.open_rename_dialog(p))
|
||||||
ui.menu_item('Mover Para...', on_click=lambda p=entry.path: self.open_move_dialog(p))
|
ui.menu_item('Mover', on_click=lambda p=entry.path: self.open_move_dialog(p))
|
||||||
ui.separator()
|
ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p))
|
||||||
ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p)).props('text-color=red')
|
|
||||||
|
|
||||||
# --- INICIALIZADOR ---
|
|
||||||
def create_ui():
|
def create_ui():
|
||||||
fm = FileManager()
|
fm = FileManager()
|
||||||
fm.container = ui.column().classes('w-full h-full p-2 md:p-4 gap-4')
|
fm.container = ui.column().classes('w-full h-full p-2 md:p-4 gap-4')
|
||||||
|
|||||||
1
data/dl_status.json
Normal file
1
data/dl_status.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"running": false, "file": "Conclu\u00eddo!", "progress": 100, "log": "Sucesso."}
|
||||||
@@ -1 +1 @@
|
|||||||
{"running": false, "file": "Cancelado pelo usu\u00e1rio \ud83d\uded1", "pct_file": 0, "pct_total": 100, "log": "Cancelado pelo usu\u00e1rio \ud83d\uded1"}
|
{"running": false, "file": "Finalizado \u2705", "pct_file": 100, "pct_total": 100, "log": "Finalizado \u2705"}
|
||||||
@@ -16,5 +16,8 @@ services:
|
|||||||
- /home/creidsu/pymediamanager/app:/app
|
- /home/creidsu/pymediamanager/app:/app
|
||||||
- /home/creidsu/pymediamanager/data:/app/data
|
- /home/creidsu/pymediamanager/data:/app/data
|
||||||
- /home/creidsu/downloads:/downloads
|
- /home/creidsu/downloads:/downloads
|
||||||
|
- /media:/media/Jellyfin
|
||||||
|
# - /media/onedrive2/Stash:/media/HD_Externo
|
||||||
|
# - /home/creidsu/outra_pasta:/media/Outros
|
||||||
ports:
|
ports:
|
||||||
- 8086:8080
|
- 8086:8080
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ pandas
|
|||||||
watchdog
|
watchdog
|
||||||
guessit
|
guessit
|
||||||
requests
|
requests
|
||||||
|
ffmpeg-python
|
||||||
|
yt-dlp
|
||||||
Reference in New Issue
Block a user