commit 56250a89ffcf577e7062ccf1cadf88e68b8aaa28 Author: Creidsu Date: Sun Jan 25 00:49:49 2026 +0000 Versão Inicial V18 (FFmpeg + Haswell) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4af9250 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# Ignorar arquivos de sistema e Python +__pycache__/ +*.py[cod] +.DS_Store + +# Ignorar dados do PyMedia Manager (Banco de dados e configs locais) +app/data/ +data/ + +# Ignorar pastas de downloads e vídeos +downloads/ +temp/ + +# Ignorar arquivos de ambiente (se tiver no futuro) +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..4693971 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +# 1. Instalar FFmpeg e APENAS o driver i965 (Haswell) +RUN apt-get update && \ + apt-get install -y \ + python3 \ + python3-pip \ + # i965 é o driver correto para 4ª Geração + i965-va-driver-shaders \ + libva-drm2 \ + libva-x11-2 \ + vainfo \ + ffmpeg \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# 2. GARANTIA: Remove qualquer vestígio do driver iHD se ele veio por dependência +RUN apt-get remove -y intel-media-va-driver intel-media-va-driver-non-free || true + +# 3. Python Setup +WORKDIR /app +COPY requirements.txt . +RUN pip3 install --no-cache-dir -r requirements.txt + +# 4. Copiar código +COPY app /app + +# Variável de ambiente fixa para o driver +ENV LIBVA_DRIVER_NAME=i965 + +CMD ["streamlit", "run", "main.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/app/main.py b/app/main.py new file mode 100755 index 0000000..d60513e --- /dev/null +++ b/app/main.py @@ -0,0 +1,26 @@ +import streamlit as st +from modules import file_manager, renamer, encoder + +st.set_page_config(page_title="PyMedia Manager", layout="wide", page_icon="🎬") + +st.title("🎬 PyMedia Manager - Central de Controle") + +# CSS para melhorar visual +st.markdown(""" + +""", unsafe_allow_html=True) + +tab1, tab2, tab3 = st.tabs(["📂 Gerenciador de Arquivos", "🏷️ Renomeador (Séries)", "⚙️ Encoder (FFmpeg)"]) + +with tab1: + file_manager.render() + +with tab2: + renamer.render() + +with tab3: + encoder.render() diff --git a/app/modules/__init__.py b/app/modules/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/app/modules/encoder.py b/app/modules/encoder.py new file mode 100755 index 0000000..fe00b6c --- /dev/null +++ b/app/modules/encoder.py @@ -0,0 +1,264 @@ +import streamlit as st +import os +import subprocess +import shutil +import json +import re +import time +import threading +from datetime import datetime + +# --- CONFIGURAÇÕES --- +ROOT_DIR = "/downloads" +OUTPUT_BASE = "/downloads/finalizados" +STATUS_FILE = "/app/data/encoder_status.json" + +# --- GERENCIAMENTO DE ESTADO EM DISCO --- +def save_status(data): + """Salva o estado atual no disco para sobreviver ao F5""" + try: + with open(STATUS_FILE, 'w') as f: + json.dump(data, f) + except: pass + +def load_status(): + """Lê o estado do disco""" + if not os.path.exists(STATUS_FILE): + return None + try: + with open(STATUS_FILE, 'r') as f: + return json.load(f) + except: + return None + +def reset_status(): + """Reseta o status para 'parado'""" + save_status({ + "is_running": False, + "current_file": "", + "progress": 0, + "total_progress": 0, + "log": "Aguardando...", + "stop_requested": False + }) + +# --- WORKER EM BACKGROUD --- +class BackgroundWorker(threading.Thread): + def __init__(self, input_folder, delete_orig): + super().__init__() + self.input_folder = input_folder + self.delete_orig = delete_orig + self.daemon = True # Daemon threads rodam em background independente da sessão + + def run(self): + # 1. Preparação + prepare_driver_environment() + + files_to_process = [] + 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_to_process.append(os.path.join(r, file)) + + total = len(files_to_process) + + # Salva estado inicial + state = { + "is_running": True, + "current_file": "Iniciando...", + "progress": 0, + "total_progress": 0, + "log": "Preparando fila...", + "stop_requested": False + } + save_status(state) + + # 2. Loop de Arquivos + for i, fpath in enumerate(files_to_process): + # Verifica se pediram pra parar + current_state = load_status() + if current_state and current_state.get("stop_requested"): + break + + fname = os.path.basename(fpath) + duration = get_video_duration(fpath) + + # Atualiza estado para arquivo atual + state["current_file"] = fname + state["progress"] = 0 + state["total_progress"] = int((i / total) * 100) + state["log"] = "Convertendo..." + save_status(state) + + # Caminhos + rel_path = os.path.relpath(fpath, self.input_folder) + folder_name = os.path.basename(self.input_folder) + if self.input_folder == ROOT_DIR: + out_full_path = os.path.join(OUTPUT_BASE, rel_path) + else: + out_full_path = os.path.join(OUTPUT_BASE, folder_name, rel_path) + + os.makedirs(os.path.dirname(out_full_path), exist_ok=True) + + # Comando FFmpeg + cmd = [ + "ffmpeg", "-y", + "-hwaccel", "vaapi", + "-hwaccel_device", "/dev/dri/renderD128", + "-hwaccel_output_format", "vaapi", + "-i", fpath + ] + cmd += get_streams_map(fpath) + cmd += [ + "-c:v", "h264_vaapi", "-b:v", "4500k", "-compression_level", "1", + "-c:a", "copy", "-c:s", "copy", + out_full_path + ] + + try: + # Executa + process = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + text=True, bufsize=1, universal_newlines=True, env=os.environ + ) + + for line in process.stdout: + # Checa stop a cada linha lida (para resposta rápida) + chk_state = load_status() + if chk_state and chk_state.get("stop_requested"): + process.terminate() + break + + if "time=" in line and duration: + match = re.search(r"time=(\d{2}:\d{2}:\d{2}\.\d{2})", line) + if match: + secs = parse_time_to_seconds(match.group(1)) + pct = int((secs / duration) * 100) + if pct > 100: pct = 100 + + # Atualiza status no disco + state["progress"] = pct + + # Pega velocidade + speed_match = re.search(r"speed=\s*(\S+)", line) + if speed_match: + state["log"] = f"Velocidade: {speed_match.group(1)}" + + save_status(state) + + process.wait() + + if process.returncode == 0: + if self.delete_orig: os.remove(fpath) + + except Exception as e: + state["log"] = f"Erro: {str(e)}" + save_status(state) + + # Fim + reset_status() + +# --- FUNÇÕES AUXILIARES --- +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 + +def get_all_subfolders(root_path): + folder_list = [root_path] + try: + for root, dirs, files in os.walk(root_path): + if "finalizados" in root or "temp" in root: continue + for d in dirs: + folder_list.append(os.path.join(root, d)) + except: pass + return sorted(folder_list) + +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) + 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 + +# --- RENDERIZAÇÃO --- +def render(): + st.header("⚙️ Encoder (Persistente)") + st.info("O processo continua rodando mesmo se você fechar a página.") + + # 1. Lê estado do disco + status = load_status() + + # Se não existe ou não está rodando + if not status or not status.get("is_running"): + all_folders = get_all_subfolders(ROOT_DIR) + idx = 0 + if 'enc_last_folder' in st.session_state and st.session_state.enc_last_folder in all_folders: + idx = all_folders.index(st.session_state.enc_last_folder) + + input_folder = st.selectbox("Selecione a pasta:", options=all_folders, index=idx, format_func=lambda x: x.replace(ROOT_DIR, "") if x != ROOT_DIR else "Raiz") + st.session_state.enc_last_folder = input_folder + + delete_orig = st.checkbox("🗑️ Excluir originais após sucesso?", value=False) + + if st.button("🚀 Iniciar Processo", type="primary"): + if not os.path.exists(input_folder): + st.error("Pasta inválida") + else: + # Inicia Thread + t = BackgroundWorker(input_folder, delete_orig) + t.start() + time.sleep(1) # Dá tempo de criar o arquivo json + st.rerun() + + else: + # MODO MONITORAMENTO + st.success("🔄 Sistema Rodando...") + + col1, col2 = st.columns([3, 1]) + with col1: + st.write(f"📁 **Processando:** `{status.get('current_file')}`") + st.progress(status.get('progress', 0), text=f"Arquivo: {status.get('progress')}%") + st.progress(status.get('total_progress', 0), text=f"Total: {status.get('total_progress')}%") + st.caption(status.get('log')) + + with col2: + if st.button("🛑 Parar", type="secondary"): + status["stop_requested"] = True + save_status(status) + st.warning("Parando...") + + if st.button("🔄 Refresh"): + st.rerun() + + # Atualiza a tela a cada 5s + time.sleep(5) + st.rerun() \ 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..616f6b1 --- /dev/null +++ b/app/modules/file_manager.py @@ -0,0 +1,223 @@ +import streamlit as st +import os +import shutil +import pandas as pd +import time +import datetime + +# Define a raiz absoluta +ROOT_DIR = "/downloads" + +def get_human_size(size): + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if size < 1024: + return f"{size:.2f} {unit}" + size /= 1024 + return f"{size:.2f} PB" + +def get_files_info(path): + files_list = [] + try: + with os.scandir(path) as entries: + # Ordena: Pastas primeiro, depois arquivos (ambos alfabéticos) + sorted_entries = sorted(entries, key=lambda e: (not e.is_dir(), e.name.lower())) + + for entry in sorted_entries: + stat = entry.stat() + dt_mod = datetime.datetime.fromtimestamp(stat.st_mtime).strftime('%d/%m/%Y %H:%M') + + info = { + "Selecionar": False, + "Tipo": "📁" if entry.is_dir() else "📄", + "Nome": entry.name, + "Tamanho": "-" if entry.is_dir() else get_human_size(stat.st_size), + "Modificado": dt_mod, + "is_dir": entry.is_dir() # Coluna oculta para lógica + } + files_list.append(info) + except Exception as e: + st.error(f"Erro ao ler pasta: {e}") + return files_list + +def get_all_subfolders(root_path): + folder_list = [root_path] + try: + for root, dirs, files in os.walk(root_path): + for d in dirs: + folder_list.append(os.path.join(root, d)) + except: pass + return sorted(folder_list) + +def render(): + st.header("📂 Explorador de Arquivos") + + # --- GESTÃO DE ESTADO (PATH) --- + if 'fm_path' not in st.session_state: + st.session_state.fm_path = ROOT_DIR + + # Segurança: não deixa subir além da raiz + if not st.session_state.fm_path.startswith(ROOT_DIR): + st.session_state.fm_path = ROOT_DIR + + current_path = st.session_state.fm_path + + # --- BARRA DE NAVEGAÇÃO SUPERIOR --- + col_nav1, col_nav2, col_nav3, col_nav4 = st.columns([1, 1, 1, 6]) + + with col_nav1: + if st.button("⬆️ Voltar", use_container_width=True, help="Subir um nível"): + parent = os.path.dirname(current_path) + st.session_state.fm_path = parent + st.rerun() + with col_nav2: + if st.button("🏠 Raiz", use_container_width=True): + st.session_state.fm_path = ROOT_DIR + st.rerun() + with col_nav3: + if st.button("🔄 Reload", use_container_width=True): + st.rerun() + with col_nav4: + # Mostra o caminho bonitinho + display_path = current_path.replace(ROOT_DIR, " / Raiz") + st.info(f"📂 **Local:** `{display_path}`") + + # --- LISTAGEM DE ARQUIVOS --- + files_data = get_files_info(current_path) + + if not files_data: + st.warning("Pasta vazia.") + df = pd.DataFrame(columns=["Selecionar", "Tipo", "Nome", "Tamanho", "Modificado"]) + else: + df = pd.DataFrame(files_data) + + # Editor de Dados (Tabela) + edited_df = st.data_editor( + df, + column_config={ + "Selecionar": st.column_config.CheckboxColumn("Sel.", width="small"), + "Tipo": st.column_config.TextColumn("", width="small"), + "Nome": st.column_config.TextColumn("Nome do Arquivo/Pasta", width="large"), + "Tamanho": st.column_config.TextColumn("Tamanho", width="medium"), + "Modificado": st.column_config.TextColumn("Data", width="medium"), + }, + disabled=["Tipo", "Nome", "Tamanho", "Modificado"], + hide_index=True, + use_container_width=True, + key=f"editor_{current_path}" # Chave única para resetar ao mudar de pasta + ) + + # Filtra Selecionados + selected_rows = edited_df[edited_df["Selecionar"] == True] + + # --- BOTÃO RÁPIDO: ABRIR PASTA --- + # Se selecionou apenas 1 item e é pasta, mostra botão gigante para entrar + if len(selected_rows) == 1: + row = selected_rows.iloc[0] + if row["is_dir"]: + st.success(f"Selecionado: 📁 {row['Nome']}") + if st.button(f"Abrir Pasta '{row['Nome']}'", type="primary", use_container_width=True): + st.session_state.fm_path = os.path.join(current_path, row["Nome"]) + st.rerun() + + st.divider() + + # --- PAINEL DE AÇÕES (ABAS) --- + tab_mov, tab_ren, tab_new, tab_del = st.tabs([ + "📦 Mover Itens", "✏️ Renomear", "➕ Criar Pasta", "🗑️ Excluir" + ]) + + # 1. ABA MOVER + with tab_mov: + st.caption("Mover itens selecionados para outra pasta.") + if selected_rows.empty: + st.info("Selecione itens na lista acima para mover.") + else: + all_folders = get_all_subfolders(ROOT_DIR) + # Remove a pasta atual da lista de destinos + all_folders = [f for f in all_folders if f != current_path] + + dest_folder = st.selectbox( + "Escolha o destino:", + all_folders, + format_func=lambda x: x.replace(ROOT_DIR, "Raiz") + ) + + if st.button("Mover Agora", type="primary"): + count = 0 + for _, row in selected_rows.iterrows(): + src = os.path.join(current_path, row["Nome"]) + dst = os.path.join(dest_folder, row["Nome"]) + try: + shutil.move(src, dst) + count += 1 + except Exception as e: + st.error(f"Erro ao mover {row['Nome']}: {e}") + + if count > 0: + st.toast(f"✅ {count} itens movidos com sucesso!") + time.sleep(1) + st.rerun() + + # 2. ABA RENOMEAR + with tab_ren: + st.caption("Renomear o item selecionado (Selecione apenas 1).") + if len(selected_rows) != 1: + st.warning("Por segurança, selecione apenas 1 item para renomear.") + else: + row = selected_rows.iloc[0] + old_name = row["Nome"] + new_name = st.text_input("Novo nome:", value=old_name) + + if new_name != old_name and st.button("Salvar Novo Nome"): + old_path = os.path.join(current_path, old_name) + new_path = os.path.join(current_path, new_name) + try: + os.rename(old_path, new_path) + st.toast(f"✅ Renomeado para {new_name}") + time.sleep(1) + st.rerun() + except Exception as e: + st.error(f"Erro: {e}") + + # 3. ABA CRIAR PASTA + with tab_new: + st.caption(f"Criar uma nova pasta dentro de: {os.path.basename(current_path)}") + new_folder_name = st.text_input("Nome da Nova Pasta:") + if st.button("Criar Pasta"): + if new_folder_name: + path_to_create = os.path.join(current_path, new_folder_name) + try: + os.makedirs(path_to_create, exist_ok=False) + st.toast(f"✅ Pasta '{new_folder_name}' criada!") + time.sleep(1) + st.rerun() + except FileExistsError: + st.error("Essa pasta já existe.") + except Exception as e: + st.error(f"Erro: {e}") + + # 4. ABA EXCLUIR + with tab_del: + if selected_rows.empty: + st.info("Selecione itens para excluir.") + else: + st.error(f"⚠️ Você tem {len(selected_rows)} itens selecionados.") + st.write("Tem certeza que deseja excluir permanentemente?") + + col_del1, col_del2 = st.columns([1, 4]) + with col_del1: + if st.button("🔥 SIM, EXCLUIR", type="primary"): + count = 0 + for _, row in selected_rows.iterrows(): + full_path = os.path.join(current_path, row["Nome"]) + try: + if row["is_dir"]: shutil.rmtree(full_path) + else: os.remove(full_path) + count += 1 + except Exception as e: + st.error(f"Erro ao deletar {row['Nome']}: {e}") + + if count > 0: + st.toast(f"🗑️ {count} itens apagados.") + time.sleep(1) + st.rerun() \ No newline at end of file diff --git a/app/modules/renamer.py b/app/modules/renamer.py new file mode 100755 index 0000000..61e58c5 --- /dev/null +++ b/app/modules/renamer.py @@ -0,0 +1,159 @@ +import streamlit as st +import os +import re +import shutil +import pandas as pd +import time + +# Define a raiz absoluta +ROOT_DIR = "/downloads" + +def get_all_subfolders(root_path): + folder_list = [root_path] + try: + for root, dirs, files in os.walk(root_path): + for d in dirs: + folder_list.append(os.path.join(root, d)) + except: pass + return sorted(folder_list) + +def extract_season_episode(filename): + """ + Testa uma lista de padrões regex para extrair Temporada e Episódio. + Retorna (season, episode) ou (None, None). + """ + # LISTA DE PADRÕES (Do mais específico para o mais genérico) + patterns = [ + # 1. Padrão Scene/Universal: S01E01, S01.E01, S01_E01, S01 - E01 + r'(?i)S(\d{1,4})[\s._-]*E(\d{1,4})', + + # 2. Padrão Anime/P2P (Seu caso): S01EP01, S01.EP01 + r'(?i)S(\d{1,4})[\s._-]*EP(\d{1,4})', + + # 3. Padrão Antigo: 1x01, 01x01 + r'(?i)(\d{1,4})x(\d{1,4})', + + # 4. Padrão Extenso: Season 1 Episode 1 + r'(?i)Season[\s._-]*(\d{1,4})[\s._-]*Episode[\s._-]*(\d{1,4})', + + # 5. Padrão Hífen (Anime): "Nome S01 - 05.mkv" + r'(?i)S(\d{1,4})[\s._-]*-\s*(\d{1,4})', + + # 6. Padrão Colchetes: [1x01] + r'(?i)\[(\d{1,4})x(\d{1,4})\]', + ] + + for pattern in patterns: + match = re.search(pattern, filename) + if match: + # Retorna Temporada, Episódio + return match.group(1), match.group(2) + + return None, None + +def render(): + st.header("🏷️ Renomeador de Séries") + st.info("Suporta: S01E01, S01EP01, 1x01, S01 - 01, Season 1 Episode 1...") + + # --- SELEÇÃO DE PASTA --- + all_folders = get_all_subfolders(ROOT_DIR) + + idx = 0 + if 'rn_last_folder' in st.session_state and st.session_state.rn_last_folder in all_folders: + idx = all_folders.index(st.session_state.rn_last_folder) + + root_path = st.selectbox( + "Selecione a pasta para analisar:", + options=all_folders, + index=idx, + format_func=lambda x: x.replace(ROOT_DIR, "") if x != ROOT_DIR else "Raiz (/downloads)" + ) + + st.session_state.rn_last_folder = root_path + + if st.button("🔍 Analisar Arquivos", type="primary"): + if not os.path.exists(root_path): + st.error("Pasta não encontrada.") + return + + preview_data = [] + ignored_files = [] + + # Varre a pasta + for root, dirs, files in os.walk(root_path): + # Ignora pastas do sistema + if "finalizados" in root or "temp" in root: continue + + for file in files: + if file.lower().endswith(('.mkv', '.mp4', '.avi')): + + # Chama a função inteligente de detecção + 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] + + season_folder = f"Temporada {s_fmt}" + new_name = f"Episódio {e_fmt}{ext}" + + original_full = os.path.join(root, file) + dest_full = os.path.join(root_path, season_folder, new_name) + + preview_data.append({ + "Arquivo Original": file, + "Nova Estrutura": f"{season_folder}/{new_name}", + "src": original_full, + "dst": dest_full + }) + except: + ignored_files.append(file) + else: + ignored_files.append(file) + + st.session_state['renamer_preview'] = preview_data + st.session_state['renamer_ignored'] = ignored_files + + # --- RESULTADOS --- + if 'renamer_preview' in st.session_state and st.session_state['renamer_preview']: + st.divider() + st.subheader(f"✅ Identificados ({len(st.session_state['renamer_preview'])})") + + df = pd.DataFrame(st.session_state['renamer_preview']) + st.dataframe( + df[["Arquivo Original", "Nova Estrutura"]], + use_container_width=True, + hide_index=True + ) + + col1, col2 = st.columns([1, 4]) + with col1: + if st.button("🚀 Confirmar", type="primary", use_container_width=True): + count = 0 + for item in st.session_state['renamer_preview']: + dst_folder = os.path.dirname(item["dst"]) + try: + if not os.path.exists(dst_folder): + os.makedirs(dst_folder, exist_ok=True) + + if not os.path.exists(item["dst"]): + shutil.move(item["src"], item["dst"]) + count += 1 + except Exception as e: + st.error(f"Erro ao mover {item['src']}: {e}") + + st.success(f"{count} arquivos organizados com sucesso!") + time.sleep(2) + del st.session_state['renamer_preview'] + del st.session_state['renamer_ignored'] + st.rerun() + + # Exibe ignorados se houver, para debug do usuário + if 'renamer_ignored' in st.session_state and st.session_state['renamer_ignored']: + with st.expander("⚠️ Arquivos ignorados (Padrão desconhecido)", expanded=False): + st.write(st.session_state['renamer_ignored']) + + elif 'renamer_preview' in st.session_state and not st.session_state['renamer_preview']: + st.warning("Nenhum arquivo de vídeo encontrado.") \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100755 index 0000000..5b7f307 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,22 @@ +version: "3.8" +services: + pymediamanager: + build: . + container_name: pymediamanager + privileged: true + restart: unless-stopped + devices: + - /dev/dri:/dev/dri + group_add: + - "993" # Grupo render/video + environment: + - TZ=America/Sao_Paulo + - LIBVA_DRIVER_NAME=i965 # Força driver Haswell + volumes: + # Mapeamento CORRIGIDO: pasta local 'app' vira '/app' no container + - /home/creidsu/pymediamanager/app:/app + - /home/creidsu/pymediamanager/data:/app/data + # Suas pastas de mídia + - /home/creidsu/downloads:/downloads + ports: + - 8501:8501 diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..5c107ce --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +streamlit +pandas +watchdog +guessit +requests +ffmpeg-python