From 53a57232a20f6144805c68f60f3538b488ecb296 Mon Sep 17 00:00:00 2001 From: Creidsu Date: Fri, 6 Feb 2026 00:17:26 +0000 Subject: [PATCH] melhorias gerais --- app/main.py | 7 +- .../__pycache__/automator.cpython-310.pyc | Bin 0 -> 17842 bytes app/modules/automator.py | 459 ++++++++++++++++++ data/automation.json | 12 + data/history.log | 6 + 5 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 app/modules/__pycache__/automator.cpython-310.pyc create mode 100644 app/modules/automator.py create mode 100644 data/automation.json diff --git a/app/main.py b/app/main.py index f31a496..e46459e 100755 --- a/app/main.py +++ b/app/main.py @@ -1,5 +1,5 @@ from nicegui import ui, app -from modules import file_manager, renamer, encoder, downloader, deployer +from modules import file_manager, renamer, encoder, downloader, deployer, automator # --- CONFIGURAÇÃO DE ARQUIVOS ESTÁTICOS --- app.add_static_files('/files', '/downloads') @@ -18,6 +18,8 @@ with ui.tabs().classes('w-full sticky top-0 z-10 bg-white shadow-sm') as tabs: t_encode = ui.tab('Encoder', icon='movie') t_down = ui.tab('Downloader', icon='download') t_deploy = ui.tab('Mover Final', icon='publish') + # NOVA ABA AQUI + t_auto = ui.tab('Automação', icon='auto_mode') # --- PAINÉIS DE CONTEÚDO --- with ui.tab_panels(tabs, value=t_files).classes('w-full p-0 pb-12'): # pb-12 dá espaço para o footer não cobrir o conteúdo @@ -36,6 +38,9 @@ with ui.tab_panels(tabs, value=t_files).classes('w-full p-0 pb-12'): # pb-12 dá with ui.tab_panel(t_deploy): deployer.create_ui() +# NOVO PAINEL AQUI + with ui.tab_panel(t_auto): + automator.create_ui() # --- RODAPÉ (FOOTER) --- # Fixo na parte inferior, mesma cor do header, texto centralizado diff --git a/app/modules/__pycache__/automator.cpython-310.pyc b/app/modules/__pycache__/automator.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdf5207aa38db620af4683408f42ba8cb51dc2f9 GIT binary patch literal 17842 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 0: + self.send_telegram(f"📋 Processando {total} arquivos aprovados.") + + for i, item in enumerate(valid_items): + src = os.path.join(item['original_root'], item['original_file']) + category = item.get('category', 'Filmes') + base_dest = self.config['destinations'].get(category) + + if not base_dest: continue + + rel_path = os.path.relpath(item['target_path'], os.path.join(renamer.DEST_DIR, category)) + final_dst = os.path.join(base_dest, rel_path) + + self.update_node_status(item['id'], 'running') + + # Encode + temp_encode_path = os.path.join('/downloads/temp_automator', os.path.basename(final_dst)) + os.makedirs(os.path.dirname(temp_encode_path), exist_ok=True) + + cmd = encoder.build_ffmpeg_command(src, temp_encode_path) + duration = encoder.get_video_duration(src) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, env=os.environ) + + success = False + for line in process.stdout: + if "time=" in line: + match = re.search(r"time=(\d{2}:\d{2}:\d{2}\.\d{2})", line) + if match: + sec = encoder.parse_time_to_seconds(match.group(1)) + pct = min(int((sec/duration)*100), 100) + self.update_node_status(item['id'], 'running', pct) + self.total_progress = (i * (100/total)) + ((100 / total) * (pct / 100)) + + process.wait() + if process.returncode == 0: success = True + else: self.update_node_status(item['id'], 'error') + + if success: + try: + os.makedirs(os.path.dirname(final_dst), exist_ok=True) + shutil.move(temp_encode_path, final_dst) + for sub in item['subtitles']: + sub_name = os.path.splitext(os.path.basename(final_dst))[0] + sub['suffix'] + sub_dst = os.path.join(os.path.dirname(final_dst), sub_name) + shutil.copy2(sub['src'], sub_dst) + self.update_node_status(item['id'], 'done', 100) + except: self.update_node_status(item['id'], 'error') + + self.send_telegram("✅ Processo concluído.") + else: + self.logs.append("Nada a processar.") + + self.tree_data[2]['status'] = 'done' + self.total_progress = 100 + + except Exception as e: + self.logs.append(f"ERRO: {str(e)}") + import traceback + print(traceback.format_exc()) + + self.is_running = False + + def build_tree_structure(self, items): + # (Mesma lógica de antes, apenas garante que usa 'visual_status' se existir) + tree = [] + categories = {} + for item in items: + cat = item.get('category') or "Pendentes" + if cat not in categories: categories[cat] = {'id': f"cat_{cat}", 'label': cat, 'children': [], 'map': {}} + + v_status = item.get('visual_status', 'pending') + # Se foi ignorado ou ambíguo não resolvido + if item.get('status') == 'SKIPPED': v_status = 'warning' + + added = False + if cat in ['Séries', 'Animes', 'Desenhos'] and item.get('target_path'): + parts = item['target_path'].split(os.sep) + if len(parts) >= 3: + show = parts[-3]; sea = parts[-2]; file = parts[-1] + if show not in categories[cat]['map']: + categories[cat]['map'][show] = {'id': f"s_{show}", 'label': show, 'children': [], 'map': {}} + categories[cat]['children'].append(categories[cat]['map'][show]) + if sea not in categories[cat]['map'][show]['map']: + categories[cat]['map'][show]['map'][sea] = {'id': f"sea_{show}_{sea}", 'label': sea, 'children': []} + categories[cat]['map'][show]['children'].append(categories[cat]['map'][show]['map'][sea]) + + categories[cat]['map'][show]['map'][sea]['children'].append({'id': item['id'], 'label': file, 'status': v_status, 'pct': 0}) + added = True + + if not added: + lbl = os.path.basename(item['original_file']) + if item.get('status') == 'SKIPPED': lbl += " (Ignorado)" + categories[cat]['children'].append({'id': item['id'], 'label': lbl, 'status': v_status, 'pct': 0}) + + for k in categories: tree.append(categories[k]) + return tree + + def update_node_status(self, fid, status, pct=0): + def search(nodes): + for node in nodes: + if node.get('id') == fid: + node['status'] = status; node['pct'] = pct; return True + if 'children' in node: + if search(node['children']): return True + return False + if len(self.tree_data) > 2: search(self.tree_data[2].get('children', [])) + + def start_process(self): + if self.is_running: return + threading.Thread(target=self.worker_thread, daemon=True).start() + + # --- UI LOOPS --- + def check_for_resolution_request(self): + """Verifica se a thread está pedindo ajuda do usuário""" + if self.waiting_for_user and (not self.dialog_resolver or not self.dialog_resolver.value): + # Se a flag tá ativa e o dialog FECHADO, abre ele + self.open_resolution_dialog() + + def update_tree_ui(self): + # Loop Principal da UI + if self.log_container: + self.log_container.clear() + for l in self.logs[-20:]: self.log_container.push(l) + + if self.btn_start: self.btn_start.set_visibility(not self.is_running) + if hasattr(self, 'progress_bar_total'): + self.progress_bar_total.value = self.total_progress / 100 + self.lbl_pct_total.text = f"{int(self.total_progress)}%" + + self.tree_container.clear() + with self.tree_container: + if not self.tree_data: ui.label('Aguardando início...').classes('text-gray-400 italic mt-10') + else: self.render_tree_recursive(self.tree_data) + + # Checa se precisa abrir o popup + self.check_for_resolution_request() + + def render_tree_recursive(self, nodes, depth=0): + # (Mesmo código de renderização anterior) + for node in nodes: + status = node.get('status', '') + pct = node.get('pct', 0) + icon = 'circle'; color = 'grey'; spin = False + if status == 'pending': icon = 'hourglass_empty' + elif status == 'running': icon = 'sync'; color = 'blue'; spin = True + elif status == 'done': icon = 'check_circle'; color = 'green' + elif status == 'error': icon = 'error'; color = 'red' + elif status == 'warning': icon = 'warning'; color = 'orange' + elif status == 'skipped': icon = 'block'; color = 'red' + + margin = f"ml-{depth * 6}" + bg = "bg-blue-100" if status == 'running' else "" + with ui.row().classes(f'w-full items-center gap-2 py-1 px-2 {margin} {bg} rounded'): + if spin: ui.spinner(size='xs').classes('mr-1') + else: ui.icon(icon, color=color, size='xs').classes('mr-1') + type_icon = 'folder' if 'children' in node else 'movie' + ui.icon(type_icon, color='amber-8' if type_icon=='folder' else 'slate-500').classes('opacity-70') + ui.label(node['label']).classes('text-sm truncate flex-grow') + if status == 'running' and 'children' not in node: + ui.linear_progress(value=pct/100, show_value=False).classes('w-24 h-3 rounded') + + if 'children' in node: self.render_tree_recursive(node['children'], depth + 1) + + def create_ui(self): + self.container = ui.column().classes('w-full h-[calc(100vh-100px)]') + self.render_layout() + + def refresh_ui(self): + if self.container: self.container.clear(); self.render_layout() + + def render_layout(self): + # (Mesmo layout anterior) + with self.container: + with ui.row().classes('w-full items-center mb-2'): + ui.icon('auto_mode', size='md', color='indigo') + ui.label('Painel de Automação').classes('text-2xl font-bold text-indigo-900') + with ui.row().classes('w-full gap-4 h-full'): + with ui.column().classes('w-full md:w-1/3 gap-2'): + with ui.card().classes('w-full bg-gray-50 p-3'): + ui.label('Configurações').classes('font-bold') + ui.input('Telegram Bot Token').bind_value(self.config, 'telegram_token').props('password dense').classes('w-full') + ui.input('Telegram Chat ID').bind_value(self.config, 'telegram_chat_id').classes('w-full mb-2') + ui.button('Salvar', on_click=self.save_config).props('flat dense icon=save color=primary w-full') + ui.separator().classes('my-2') + ui.label('Monitorar:').classes('text-xs font-bold') + ui.button(self.config['monitor_folder'], icon='folder', on_click=lambda: self.pick_folder(target_key='monitor_folder', start_path='/downloads')).props('flat dense align=left w-full text-xs bg-white border') + ui.label('Destinos:').classes('text-xs font-bold mt-2') + for cat in ['Filmes', 'Séries', 'Animes', 'Desenhos']: + dest = self.config['destinations'].get(cat, '?') + ui.button(f"{cat}: {os.path.basename(dest)}", icon='arrow_forward', on_click=lambda c=cat: self.pick_folder(key_category='destinations', target_key=c, start_path='/media')).props('flat dense align=left w-full text-xs bg-white border') + with ui.card().classes('w-full flex-grow bg-slate-900 text-white p-2'): + ui.label('Log do Sistema').classes('text-xs font-bold text-gray-400') + self.log_container = ui.log().classes('w-full h-full font-mono text-[10px]') + with ui.card().classes('w-full md:w-2/3 h-full border-t-4 border-indigo-500 flex flex-col'): + with ui.column().classes('w-full border-b pb-4 mb-2'): + with ui.row().classes('w-full justify-between items-center'): + ui.label('Status da Fila').classes('font-bold text-lg') + self.btn_start = ui.button('INICIAR', on_click=self.start_process).props('color=indigo icon=play_arrow') + with ui.row().classes('w-full items-center gap-2'): + self.progress_bar_total = ui.linear_progress(value=0).classes('flex-grow h-6 rounded-lg') + self.lbl_pct_total = ui.label('0%').classes('font-bold min-w-[3rem] text-right') + self.tree_container = ui.column().classes('w-full flex-grow overflow-y-auto p-2 bg-gray-50 rounded border') + ui.timer(0.5, self.update_tree_ui) + +manager = AutomationManager() +def create_ui(): manager.create_ui() \ No newline at end of file diff --git a/data/automation.json b/data/automation.json new file mode 100644 index 0000000..8d2e9d6 --- /dev/null +++ b/data/automation.json @@ -0,0 +1,12 @@ +{ + "telegram_token": "8543089985:AAFgDMOg9VlikAsPTi35FcF395W2uAtSzuY", + "telegram_chat_id": "8392926121", + "monitor_folder": "/downloads/completos", + "destinations": { + "Filmes": "/media/Jellyfin/onedrive/Jellyfin/Filmes", + "S\u00e9ries": "/media/Jellyfin/onedrive/Jellyfin/Series", + "Animes": "/media/Jellyfin/onedrive/Jellyfin/Animes", + "Desenhos": "/media/Jellyfin/onedrive/Jellyfin/Desenhos" + }, + "tmdb_api_key_view": "12856f632876dc743b6f6775f4e5bd7d" +} \ No newline at end of file diff --git a/data/history.log b/data/history.log index 03b4897..7951228 100644 --- a/data/history.log +++ b/data/history.log @@ -50,3 +50,9 @@ Traceback (most recent call last): OSError: [Errno 107] Transport endpoint is not connected: '/media/Jellyfin/onedrive/Jellyfin/Filmes/Se Esse Amor Desaparecesse Hoje (2025).mkv' [2026-02-04 22:50:37] ✅ Movido: Se Esse Amor Desaparecesse Hoje (2025).mkv +[2026-02-05 13:23:06] ✅ Movido: Episódio 17.mp4 +[2026-02-05 13:23:09] ✅ Movido: Episódio 03.mp4 +[2026-02-05 13:23:11] ✅ Movido: Episódio 06.mp4 +[2026-02-05 13:23:13] ✅ Movido: Episódio 07.mp4 +[2026-02-05 13:23:13] ⚠️ Pendência: Episódio 14.mp4 +[2026-02-05 13:23:27] 🔄 Substituído: Episódio 14.mp4