From 56df0b1d1f129e99d3cc9b4a0f0ed55e660a4d5b Mon Sep 17 00:00:00 2001 From: Creidsu Date: Mon, 26 Jan 2026 23:07:17 +0000 Subject: [PATCH] =?UTF-8?q?mudado=20o=20motor=20de=20renderiza=C3=A7=C3=A3?= =?UTF-8?q?o=20para=20o=20nicegui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 16 + app/main.py | 33 ++ app/modules/__init__.py | 0 .../__pycache__/__init__.cpython-310.pyc | Bin 0 -> 117 bytes .../__pycache__/encoder.cpython-310.pyc | Bin 0 -> 10616 bytes .../__pycache__/file_manager.cpython-310.pyc | Bin 0 -> 11686 bytes .../__pycache__/renamer.cpython-310.pyc | Bin 0 -> 7086 bytes app/modules/encoder.py | 287 ++++++++++++++++++ app/modules/file_manager.py | 248 +++++++++++++++ app/modules/renamer.py | 181 +++++++++++ data/status.json | 1 + docker-compose.yml | 20 ++ requirements.txt | 5 + 13 files changed, 791 insertions(+) create mode 100755 Dockerfile create mode 100755 app/main.py create mode 100755 app/modules/__init__.py create mode 100644 app/modules/__pycache__/__init__.cpython-310.pyc create mode 100644 app/modules/__pycache__/encoder.cpython-310.pyc create mode 100644 app/modules/__pycache__/file_manager.cpython-310.pyc create mode 100644 app/modules/__pycache__/renamer.cpython-310.pyc create mode 100755 app/modules/encoder.py create mode 100755 app/modules/file_manager.py create mode 100755 app/modules/renamer.py create mode 100644 data/status.json create mode 100755 docker-compose.yml create mode 100755 requirements.txt diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..99af754 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM ubuntu:22.04 +ENV DEBIAN_FRONTEND=noninteractive + +# Instala FFmpeg, Drivers Intel e Python +RUN apt-get update && \ + apt-get install -y python3 python3-pip intel-media-va-driver-non-free i965-va-driver-shaders libva-drm2 libva-x11-2 vainfo ffmpeg jq && \ + rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt + +COPY app /app + +# NiceGUI roda na porta 8080 por padrão +CMD ["python3", "main.py"] diff --git a/app/main.py b/app/main.py new file mode 100755 index 0000000..9872ad3 --- /dev/null +++ b/app/main.py @@ -0,0 +1,33 @@ +from nicegui import ui, app +from modules import file_manager, renamer, encoder + +# Configuração Geral +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: + t_files = ui.tab('Gerenciador', icon='folder') + t_rename = ui.tab('Renomeador', icon='edit') + t_encode = ui.tab('Encoder', icon='movie') + +# Painéis +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'): + file_manager.create_ui() + + # PAINEL 2: RENAMER + with ui.tab_panel(t_rename): + renamer.create_ui() + + # PAINEL 3: ENCODER + with ui.tab_panel(t_encode): + encoder.create_ui() + +ui.run(title='PyMedia Manager', port=8080, reload=True, storage_secret='secret') diff --git a/app/modules/__init__.py b/app/modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/app/modules/__pycache__/__init__.cpython-310.pyc b/app/modules/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a1127b1634a9265b1c98912ac49bbd26243b099 GIT binary patch literal 117 zcmd1j<>g`kf_MAMGC}lX5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!H7equp^er|qB iX-;afetdjpUS>&ryk0@&Ee;!qs2xa0F%ytrVE_P9%@$_> literal 0 HcmV?d00001 diff --git a/app/modules/__pycache__/encoder.cpython-310.pyc b/app/modules/__pycache__/encoder.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b70c4a47c029171eecda1d4f65e6d7ecd0a9472 GIT binary patch literal 10616 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 H@%{e+CHZJP literal 0 HcmV?d00001 diff --git a/app/modules/__pycache__/renamer.cpython-310.pyc b/app/modules/__pycache__/renamer.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cc7023356f2784044a7b9dfb17418afde9e4a52 GIT binary patch literal 7086 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 literal 0 HcmV?d00001 diff --git a/app/modules/encoder.py b/app/modules/encoder.py new file mode 100755 index 0000000..cf869a2 --- /dev/null +++ b/app/modules/encoder.py @@ -0,0 +1,287 @@ +from nicegui import ui, app +import os +import threading +import time +import subprocess +import json +import re + +ROOT_DIR = "/downloads" +OUTPUT_BASE = "/downloads/finalizados" +STATUS_FILE = "/app/data/status.json" + +# --- BACKEND: PREPARAÇÃO DE DRIVERS --- +def prepare_driver_environment(): + os.environ["LIBVA_DRIVER_NAME"] = "i965" + drivers_ruins = ["/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so", "/usr/lib/x86_64-linux-gnu/dri/iHD_drv_video.so.1"] + for driver in drivers_ruins: + if os.path.exists(driver): + try: os.remove(driver) + except: pass + +# --- BACKEND: UTILS FFMPEG --- +def get_video_duration(filepath): + cmd = ["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filepath] + try: return float(subprocess.check_output(cmd).decode().strip()) + except: return None + +def parse_time_to_seconds(time_str): + h, m, s = time_str.split(':') + return int(h) * 3600 + int(m) * 60 + float(s) + +def get_streams_map(filepath): + cmd = ["ffprobe", "-v", "quiet", "-print_format", "json", "-show_streams", filepath] + try: + res = subprocess.run(cmd, capture_output=True, text=True, env=os.environ) + data = json.loads(res.stdout) + except: return ["-map", "0"] + + map_args = ["-map", "0:v"] + audio_found = False + for s in data.get('streams', []): + if s['codec_type'] == 'audio': + lang = s.get('tags', {}).get('language', 'und').lower() + if lang in ['por', 'pt', 'eng', 'en', 'jpn', 'ja', 'und']: + map_args.extend(["-map", f"0:{s['index']}"]) + audio_found = True + if not audio_found: map_args.extend(["-map", "0:a"]) + + for s in data.get('streams', []): + if s['codec_type'] == 'subtitle': + lang = s.get('tags', {}).get('language', 'und').lower() + if lang in ['por', 'pt', 'pob', 'pt-br']: + map_args.extend(["-map", f"0:{s['index']}"]) + return map_args + +# --- BACKEND: WORKER THREAD --- +class EncoderWorker(threading.Thread): + def __init__(self, input_folder): + super().__init__() + self.input_folder = input_folder + self.daemon = True + + def run(self): + prepare_driver_environment() + + files = [] + for r, d, f in os.walk(self.input_folder): + if "finalizados" in r or "temp" in r: continue + for file in f: + if file.lower().endswith(('.mkv', '.mp4', '.avi')): + files.append(os.path.join(r, file)) + + total_files = len(files) + stop_signal = False + + for i, fpath in enumerate(files): + # Verifica Parada antes de começar o próximo + if os.path.exists(STATUS_FILE): + with open(STATUS_FILE, 'r') as f: + if json.load(f).get('stop_requested'): + stop_signal = True + break + + fname = os.path.basename(fpath) + + # Status Inicial + status = { + "running": True, + "stop_requested": False, + "file": fname, + "pct_file": 0, + "pct_total": int((i / total_files) * 100), + "current_index": i + 1, + "total_files": total_files, + "log": "Iniciando..." + } + with open(STATUS_FILE, 'w') as f: json.dump(status, f) + + rel = os.path.relpath(fpath, self.input_folder) + out = os.path.join(OUTPUT_BASE, os.path.basename(self.input_folder), rel) + os.makedirs(os.path.dirname(out), exist_ok=True) + + map_args = get_streams_map(fpath) + cmd = [ + "ffmpeg", "-y", "-hwaccel", "vaapi", "-hwaccel_device", "/dev/dri/renderD128", + "-hwaccel_output_format", "vaapi", "-i", fpath + ] + cmd += map_args + cmd += [ + "-c:v", "h264_vaapi", "-qp", "25", "-compression_level", "0", + "-c:a", "copy", "-c:s", "copy", out + ] + + total_sec = get_video_duration(fpath) or 1 + + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ) + + for line in proc.stdout: + # Verifica Parada DURANTE a conversão + if "time=" in line: # Checa a cada atualização de tempo + if os.path.exists(STATUS_FILE): + with open(STATUS_FILE, 'r') as f: + if json.load(f).get('stop_requested'): + proc.terminate() # Mata o FFmpeg + stop_signal = True + break + + match = re.search(r"time=(\d{2}:\d{2}:\d{2}\.\d{2})", line) + if match: + sec = parse_time_to_seconds(match.group(1)) + pct = min(int((sec/total_sec)*100), 100) + status["pct_file"] = pct + speed = re.search(r"speed=\s*(\S+)", line) + if speed: status["log"] = f"Velocidade: {speed.group(1)}" + with open(STATUS_FILE, 'w') as f: json.dump(status, f) + + proc.wait() + + if stop_signal: + # Limpa arquivo incompleto se foi cancelado + if os.path.exists(out): os.remove(out) + break + + # Status Final + final_msg = "Cancelado pelo usuário 🛑" if stop_signal else "Finalizado ✅" + with open(STATUS_FILE, 'w') as f: + json.dump({"running": False, "file": final_msg, "pct_file": 0 if stop_signal else 100, "pct_total": 100, "log": final_msg}, f) + +# --- FRONTEND: UI --- +class EncoderInterface: + def __init__(self): + self.path = ROOT_DIR + self.container = None + self.view_mode = 'explorer' + self.timer = None + + 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_monitor() + + def start_encoding(self): + if os.path.exists(STATUS_FILE): os.remove(STATUS_FILE) + t = EncoderWorker(self.path) + t.start() + ui.notify('Iniciado!', type='positive') + self.view_mode = 'monitor' + self.refresh() + + def stop_encoding(self): + # Escreve o sinal de parada no arquivo JSON + if os.path.exists(STATUS_FILE): + try: + with open(STATUS_FILE, 'r+') as f: + data = json.load(f) + data['stop_requested'] = True + f.seek(0) + json.dump(data, f) + f.truncate() + ui.notify('Parando processo... aguarde.', type='warning') + except: pass + + def back_to_explorer(self): + self.view_mode = 'explorer' + self.refresh() + + def render_breadcrumbs(self): + with ui.row().classes('w-full items-center bg-gray-100 p-2 rounded gap-1'): + ui.button('🏠', on_click=lambda: self.navigate(ROOT_DIR)).props('flat dense text-color=grey-8') + 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) + ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps text-color=primary') + ui.space() + ui.button("🚀 Converter Esta Pasta", on_click=self.start_encoding).props('push color=primary') + + def render_folder_list(self): + try: + 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: return + with ui.column().classes('w-full gap-1 mt-2'): + 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') + 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') + + def render_monitor(self): + ui.label('Monitor de Conversão').classes('text-xl font-bold mb-4') + + lbl_file = ui.label('Inicializando...') + progress_file = ui.linear_progress(value=0).classes('w-full') + lbl_status = ui.label('---') + + ui.separator().classes('my-4') + + lbl_total = ui.label('Total: 0/0') + progress_total = ui.linear_progress(value=0).classes('w-full') + + # Botões de Controle + row_btns = ui.row().classes('mt-4 gap-2') + + # Botão de Parar (Só aparece se estiver rodando) + btn_stop = ui.button('🛑 Parar Processo', on_click=self.stop_encoding).props('color=red') + # Botão Voltar (Só aparece se acabou) + btn_back = ui.button('Voltar para Pastas', on_click=self.back_to_explorer).props('outline') + btn_back.set_visibility(False) + + def update_loop(): + if not os.path.exists(STATUS_FILE): return + try: + with open(STATUS_FILE, 'r') as f: data = json.load(f) + + is_running = data.get('running', False) + + lbl_file.text = f"Arquivo: {data.get('file', '?')}" + val_file = data.get('pct_file', 0) / 100 + progress_file.value = val_file + lbl_status.text = f"Status: {int(val_file*100)}% | {data.get('log', '')}" + + if 'total_files' in data: + curr = data.get('current_index', 0) + tot = data.get('total_files', 0) + lbl_total.text = f"Fila: {curr} de {tot} arquivos" + val_total = data.get('pct_total', 0) / 100 + progress_total.value = val_total + + # Controle de Visibilidade dos Botões + if is_running: + btn_stop.set_visibility(True) + btn_back.set_visibility(False) + else: + btn_stop.set_visibility(False) + btn_back.set_visibility(True) + + except: pass + + self.timer = ui.timer(1.0, update_loop) + +def create_ui(): + enc = EncoderInterface() + if os.path.exists(STATUS_FILE): + try: + with open(STATUS_FILE, 'r') as f: + if json.load(f).get('running'): enc.view_mode = 'monitor' + except: pass + enc.container = ui.column().classes('w-full h-full p-4 gap-4') + enc.refresh() \ No newline at end of file diff --git a/app/modules/file_manager.py b/app/modules/file_manager.py new file mode 100755 index 0000000..699d220 --- /dev/null +++ b/app/modules/file_manager.py @@ -0,0 +1,248 @@ +from nicegui import ui +import os +import shutil +import datetime + +ROOT_DIR = "/downloads" + +# --- UTILITÁRIOS --- +def get_human_size(size): + for unit in ['B', 'KB', 'MB', 'GB']: + if size < 1024: return f"{size:.2f} {unit}" + size /= 1024 + return f"{size:.2f} TB" + +def get_subfolders(root): + folders = [root] + try: + for r, d, f in os.walk(root): + if "finalizados" in r or "temp" in r: continue + for folder in d: + if not folder.startswith('.'): folders.append(os.path.join(r, folder)) + except: pass + return sorted(folders) + +# --- CLASSE GERENCIADORA --- +class FileManager: + def __init__(self): + self.path = ROOT_DIR + self.view_mode = 'grid' + self.container = None + + def navigate(self, path): + if os.path.exists(path) and os.path.isdir(path): + self.path = path + self.refresh() + else: + ui.notify('Caminho inválido', type='negative') + + def navigate_up(self): + parent = os.path.dirname(self.path) + if self.path != ROOT_DIR: + self.navigate(parent) + + def toggle_view(self): + self.view_mode = 'list' if self.view_mode == 'grid' else 'grid' + self.refresh() + + def refresh(self): + if self.container: + self.container.clear() + with self.container: + self.render_header() + self.render_content() + + # --- DIÁLOGOS DE AÇÃO (ORDEM CORRIGIDA) --- + def open_delete_dialog(self, path): + with ui.dialog() as dialog, ui.card(): + ui.label('Excluir item permanentemente?').classes('text-lg font-bold') + ui.label(os.path.basename(path)) + with ui.row().classes('w-full justify-end'): + ui.button('Cancelar', on_click=dialog.close).props('flat') + def confirm(): + try: + if os.path.isdir(path): shutil.rmtree(path) + else: os.remove(path) + # 1. Notifica e Fecha ANTES de destruir a UI + ui.notify('Excluído!', type='positive') + dialog.close() + # 2. Atualiza a tela (Destrói elementos antigos) + self.refresh() + except Exception as e: ui.notify(str(e), type='negative') + ui.button('Excluir', on_click=confirm).props('color=red') + dialog.open() + + def open_rename_dialog(self, path): + with ui.dialog() as dialog, ui.card(): + ui.label('Renomear').classes('text-lg') + name_input = ui.input('Novo Nome', value=os.path.basename(path)).classes('w-full') + def save(): + try: + new_path = os.path.join(os.path.dirname(path), name_input.value) + os.rename(path, new_path) + ui.notify('Renomeado!', type='positive') + dialog.close() + self.refresh() + except Exception as e: ui.notify(str(e), type='negative') + ui.button('Salvar', on_click=save).props('color=primary') + dialog.open() + + def open_move_dialog(self, path): + folders = get_subfolders(ROOT_DIR) + 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} + + with ui.dialog() as dialog, ui.card().classes('w-96'): + ui.label('Mover Para...').classes('text-lg') + target = ui.select(opts, value=ROOT_DIR, with_input=True).classes('w-full') + def confirm(): + try: + shutil.move(path, target.value) + ui.notify('Movido!', type='positive') + dialog.close() + self.refresh() + except Exception as e: ui.notify(str(e), type='negative') + ui.button('Mover', on_click=confirm).props('color=primary') + dialog.open() + + def open_create_folder(self): + with ui.dialog() as dialog, ui.card(): + ui.label('Nova Pasta') + name = ui.input('Nome') + def create(): + try: + os.makedirs(os.path.join(self.path, name.value)) + ui.notify('Pasta criada!', type='positive') + dialog.close() + self.refresh() + except Exception as e: ui.notify(str(e), type='negative') + ui.button('Criar', on_click=create) + dialog.open() + + # --- MENU DE CONTEXTO (CORRIGIDO) --- + def bind_context_menu(self, element, entry): + """ + CORREÇÃO: Usa 'contextmenu.prevent' para bloquear o menu do navegador. + """ + with ui.menu() as m: + 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.separator() + 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()) + + # --- RENDERIZADORES --- + def render_header(self): + with ui.row().classes('w-full items-center bg-gray-100 p-2 rounded-lg gap-2'): + if self.path != ROOT_DIR: + ui.button(icon='arrow_upward', on_click=self.navigate_up).props('flat round dense').tooltip('Subir') + else: + ui.button(icon='home').props('flat round dense disabled text-color=grey') + + # Breadcrumbs + rel = os.path.relpath(self.path, ROOT_DIR) + parts = rel.split(os.sep) if rel != '.' else [] + + 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') + acc = ROOT_DIR + for part in parts: + acc = os.path.join(acc, part) + ui.label('/') + ui.button(part, on_click=lambda p=acc: self.navigate(p)).props('flat dense no-caps min-w-0 px-2') + + ui.space() + ui.button(icon='create_new_folder', on_click=self.open_create_folder).props('flat round dense').tooltip('Nova Pasta') + + 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') + + def render_content(self): + try: + entries = sorted(os.scandir(self.path), key=lambda e: (not e.is_dir(), e.name.lower())) + except: + ui.label('Erro ao ler pasta').classes('text-red') + return + + if not entries: + ui.label('Pasta vazia').classes('w-full text-center text-gray-400 mt-10') + return + + # === 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'): + 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', '.avi')): icon = 'movie' + color = 'amber-8' if is_dir else 'blue-grey' + 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: + if is_dir: card.on('click', lambda p=entry.path: self.navigate(p)) + + # CORREÇÃO: Bind correto do menu de contexto + self.bind_context_menu(card, entry) + + 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') + + 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.menu(): + 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.separator() + ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p)).props('text-color=red') + + # === LIST === + else: + 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: + is_dir = entry.is_dir() + icon = 'folder' if is_dir else 'description' + color = 'amber-8' if is_dir else 'blue-grey' + + 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)) + + self.bind_context_menu(row, entry) + + ui.icon(icon, color=color).classes('mr-2') + + with ui.column().classes('flex-grow gap-0'): + 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') + + 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.menu(): + 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.separator() + ui.menu_item('Excluir', on_click=lambda p=entry.path: self.open_delete_dialog(p)).props('text-color=red') + +# --- INICIALIZADOR --- +def create_ui(): + fm = FileManager() + fm.container = ui.column().classes('w-full h-full p-2 md:p-4 gap-4') + fm.refresh() \ No newline at end of file diff --git a/app/modules/renamer.py b/app/modules/renamer.py new file mode 100755 index 0000000..45c1690 --- /dev/null +++ b/app/modules/renamer.py @@ -0,0 +1,181 @@ +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() \ No newline at end of file diff --git a/data/status.json b/data/status.json new file mode 100644 index 0000000..3331ef4 --- /dev/null +++ b/data/status.json @@ -0,0 +1 @@ +{"running": false, "file": "Cancelado pelo usu\u00e1rio \ud83d\uded1", "pct_file": 0, "pct_total": 100, "log": "Cancelado pelo usu\u00e1rio \ud83d\uded1"} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..cc64ac5 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ +version: "3.8" +services: + pymediamanager: + build: . + container_name: pymediamanager + privileged: true + restart: unless-stopped + devices: + - /dev/dri:/dev/dri + group_add: + - "993" + environment: + - TZ=America/Sao_Paulo + - LIBVA_DRIVER_NAME=i965 + volumes: + - /home/creidsu/pymediamanager/app:/app + - /home/creidsu/pymediamanager/data:/app/data + - /home/creidsu/downloads:/downloads + ports: + - 8086:8080 diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..8e1b4be --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +nicegui +pandas +watchdog +guessit +requests