corrigido o merger para não perder pastas inteiras

This commit is contained in:
2026-02-08 22:45:43 +00:00
parent 53a57232a2
commit 3d77959d1d
3 changed files with 82 additions and 100 deletions

View File

@@ -4,13 +4,13 @@ import shutil
import json import json
import asyncio import asyncio
import datetime import datetime
from collections import deque # Import necessário para ler as últimas linhas de forma eficiente from collections import deque
# --- CONFIGURAÇÕES DE DIRETÓRIOS --- # --- CONFIGURAÇÕES DE DIRETÓRIOS ---
SRC_ROOT = "/downloads" SRC_ROOT = "/downloads"
DST_ROOT = "/media" DST_ROOT = "/media"
CONFIG_PATH = "/app/data/presets.json" CONFIG_PATH = "/app/data/presets.json"
LOG_PATH = "/app/data/history.log" # Novo arquivo de log persistente LOG_PATH = "/app/data/history.log"
class DeployManager: class DeployManager:
def __init__(self): def __init__(self):
@@ -20,123 +20,124 @@ class DeployManager:
self.container = None self.container = None
self.presets = self.load_presets() self.presets = self.load_presets()
self.pendencies = [] self.pendencies = []
# CARREGA OS LOGS DO ARQUIVO AO INICIAR
self.logs = self.load_logs_from_file() self.logs = self.load_logs_from_file()
# --- NOVO: GERENCIAMENTO DE LOGS PERSISTENTES --- # --- GERENCIAMENTO DE LOGS ---
def load_logs_from_file(self): def load_logs_from_file(self):
"""Lê as últimas 50 linhas do arquivo de log"""
if not os.path.exists(LOG_PATH): if not os.path.exists(LOG_PATH):
return [] return []
try: try:
# Lê as últimas 50 linhas do arquivo
with open(LOG_PATH, 'r', encoding='utf-8') as f: with open(LOG_PATH, 'r', encoding='utf-8') as f:
# deque(..., maxlen=50) pega automaticamente as últimas 50
last_lines = list(deque(f, maxlen=50)) last_lines = list(deque(f, maxlen=50))
# Inverte a ordem para mostrar o mais recente no topo da lista visual
# remove quebras de linha com .strip()
return [line.strip() for line in reversed(last_lines)] return [line.strip() for line in reversed(last_lines)]
except: except:
return [] return []
def add_log(self, message, type="info"): def add_log(self, message, type="info"):
"""Adiciona log na memória E no arquivo"""
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
full_msg = f"[{timestamp}] {message}" full_msg = f"[{timestamp}] {message}"
# 1. Atualiza a lista da memória (para a UI)
self.logs.insert(0, full_msg) self.logs.insert(0, full_msg)
if len(self.logs) > 50: self.logs.pop() # Limpa memória excedente if len(self.logs) > 50: self.logs.pop()
# 2. Salva no arquivo (Modo 'a' = append/adicionar no fim)
try: try:
with open(LOG_PATH, 'a', encoding='utf-8') as f: with open(LOG_PATH, 'a', encoding='utf-8') as f:
f.write(full_msg + "\n") f.write(full_msg + "\n")
except Exception as e: except: pass
print(f"Erro ao salvar log: {e}")
# --- 1. PERSISTÊNCIA (JSON) --- # --- 1. PERSISTÊNCIA ---
def load_presets(self): def load_presets(self):
if os.path.exists(CONFIG_PATH): if os.path.exists(CONFIG_PATH):
try: try:
with open(CONFIG_PATH, 'r') as f: with open(CONFIG_PATH, 'r') as f: return json.load(f)
return json.load(f)
except: return {} except: return {}
return {} return {}
def save_preset(self, name): def save_preset(self, name):
if not name: return if not name: return
self.presets[name] = {'src': self.src_path, 'dst': self.dst_path} self.presets[name] = {'src': self.src_path, 'dst': self.dst_path}
with open(CONFIG_PATH, 'w') as f: with open(CONFIG_PATH, 'w') as f: json.dump(self.presets, f)
json.dump(self.presets, f)
ui.notify(f'Preset "{name}" salvo!') ui.notify(f'Preset "{name}" salvo!')
self.refresh() self.refresh()
def delete_preset(self, name): def delete_preset(self, name):
if name in self.presets: if name in self.presets:
del self.presets[name] del self.presets[name]
with open(CONFIG_PATH, 'w') as f: with open(CONFIG_PATH, 'w') as f: json.dump(self.presets, f)
json.dump(self.presets, f)
self.refresh() self.refresh()
# --- 2. DIÁLOGO DE CONFIRMAÇÃO DO PRESET --- # --- 2. DIÁLOGO ---
def confirm_preset_execution(self, name, paths): def confirm_preset_execution(self, name, paths):
src = paths['src'] src, dst = paths['src'], paths['dst']
dst = paths['dst']
if not os.path.exists(src): if not os.path.exists(src):
ui.notify(f'Erro: Pasta de origem não existe: {src}', type='negative') ui.notify(f'Erro: Origem não existe: {src}', type='negative')
return return
try: try: count = len([f for f in os.listdir(src) if not f.startswith('.')])
items = [f for f in os.listdir(src) if not f.startswith('.')]
count = len(items)
except: count = 0 except: count = 0
with ui.dialog() as dialog, ui.card(): with ui.dialog() as dialog, ui.card():
ui.label(f'Executar: {name}?').classes('text-xl font-bold text-blue-900') ui.label(f'Executar: {name}?').classes('text-xl font-bold text-blue-900')
ui.label(f'Origem: {src}').classes('text-xs bg-gray-100 p-1 w-full break-all')
ui.label(f'Origem: {src}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all') ui.label(f'Destino: {dst}').classes('text-xs bg-gray-100 p-1 w-full break-all')
ui.label(f'Destino: {dst}').classes('font-mono text-xs bg-gray-100 p-1 rounded w-full break-all') ui.label(f'{count} itens encontrados.').classes('font-bold text-green-700 mt-2') if count > 0 else None
if count > 0:
ui.label(f'{count} itens encontrados.').classes('font-bold text-green-700 mt-2')
else:
ui.label('Atenção: A pasta de origem parece vazia.').classes('font-bold text-orange-600 mt-2')
# Função wrapper para rodar o async corretamente
async def execute_action(): async def execute_action():
dialog.close() dialog.close()
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
await self.move_process_from_preset(paths) await self.move_process_from_preset(paths)
with ui.row().classes('w-full justify-end mt-4'): with ui.row().classes('w-full justify-end mt-4'):
ui.button('Cancelar', on_click=dialog.close).props('flat text-color=grey') ui.button('Cancelar', on_click=dialog.close).props('flat')
ui.button('CONFIRMAR', on_click=execute_action).props('color=green icon=check') ui.button('CONFIRMAR', on_click=execute_action).props('color=green')
dialog.open() dialog.open()
# --- 3. MOVIMENTAÇÃO E PENDÊNCIAS --- # --- 3. MOVIMENTAÇÃO BLINDADA (SAFE MERGE) ---
async def move_process(self, items_to_move, target_folder): async def move_process(self, items_to_move, target_folder):
"""
Nova lógica: Nunca move pastas inteiras.
Sempre cria a estrutura no destino e move apenas arquivos.
"""
for item_path in items_to_move: for item_path in items_to_move:
if not os.path.exists(item_path): continue if not os.path.exists(item_path): continue
name = os.path.basename(item_path) name = os.path.basename(item_path)
destination = os.path.join(target_folder, name) destination = os.path.join(target_folder, name)
# --- CASO 1: É UMA PASTA? ---
if os.path.isdir(item_path):
# Não verifica se existe ou não. Simplesmente garante que a pasta
# exista no destino (mkdir -p) e entra nela.
try:
if not os.path.exists(destination):
os.makedirs(destination, exist_ok=True)
self.apply_permissions(destination) # Garante permissão na nova pasta
# Pega conteúdo e RECURSIVIDADE (mergulha na pasta)
sub_items = [os.path.join(item_path, f) for f in os.listdir(item_path)]
await self.move_process(sub_items, destination)
# Limpeza: Se a pasta de origem ficou vazia, remove ela
if not os.listdir(item_path):
os.rmdir(item_path)
except Exception as e:
self.add_log(f"❌ Erro na pasta {name}: {e}", "negative")
continue
# --- CASO 2: É UM ARQUIVO? ---
# Se já existe o arquivo exato no destino -> Pendência
if os.path.exists(destination): if os.path.exists(destination):
self.add_log(f"⚠️ Pendência: {name}", "warning") self.add_log(f"⚠️ Conflito de arquivo: {name}", "warning")
self.pendencies.append({'name': name, 'src': item_path, 'dst': destination}) self.pendencies.append({'name': name, 'src': item_path, 'dst': destination})
self.refresh() self.refresh()
continue continue
# Se não existe -> Move o arquivo
try: try:
await run.cpu_bound(shutil.move, item_path, destination) await run.cpu_bound(shutil.move, item_path, destination)
self.apply_permissions(destination) self.apply_permissions(destination)
self.add_log(f"Movido: {name}", "positive") self.add_log(f"Arquivo movido: {name}", "positive")
except Exception as e: except Exception as e:
self.add_log(f"❌ Erro em {name}: {e}", "negative") self.add_log(f"❌ Erro arquivo {name}: {e}", "negative")
self.selected_items = [] self.selected_items = []
self.refresh() self.refresh()
@@ -146,7 +147,7 @@ class DeployManager:
if os.path.exists(src): if os.path.exists(src):
items = [os.path.join(src, f) for f in os.listdir(src)] items = [os.path.join(src, f) for f in os.listdir(src)]
await self.move_process(items, dst) await self.move_process(items, dst)
else: ui.notify('Origem do preset não encontrada!', type='negative') else: ui.notify('Origem não encontrada!', type='negative')
def apply_permissions(self, path): def apply_permissions(self, path):
try: try:
@@ -154,7 +155,7 @@ class DeployManager:
else: os.chmod(path, 0o777) else: os.chmod(path, 0o777)
except: pass except: pass
# --- 4. AÇÕES EM MASSA PARA PENDÊNCIAS --- # --- 4. AÇÕES PENDÊNCIAS ---
async def handle_all_pendencies(self, action): async def handle_all_pendencies(self, action):
temp_list = list(self.pendencies) temp_list = list(self.pendencies)
for i in range(len(temp_list)): for i in range(len(temp_list)):
@@ -167,6 +168,7 @@ class DeployManager:
if action == 'replace': if action == 'replace':
try: try:
# Como agora só arquivos caem aqui, remove e substitui
if os.path.isdir(item['dst']): if os.path.isdir(item['dst']):
await run.cpu_bound(shutil.rmtree, item['dst']) await run.cpu_bound(shutil.rmtree, item['dst'])
else: else:
@@ -184,7 +186,6 @@ class DeployManager:
def render_breadcrumbs(self, current_path, root_dir, nav_callback): 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 mb-2'): with ui.row().classes('items-center gap-1 bg-gray-100 p-1 rounded w-full mb-2'):
ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm') ui.button('🏠', on_click=lambda: nav_callback(root_dir)).props('flat dense size=sm')
rel = os.path.relpath(current_path, root_dir) rel = os.path.relpath(current_path, root_dir)
if rel != '.': if rel != '.':
acc = root_dir acc = root_dir
@@ -192,7 +193,6 @@ class DeployManager:
ui.label('/') ui.label('/')
acc = os.path.join(acc, part) acc = os.path.join(acc, part)
ui.button(part, on_click=lambda p=acc: nav_callback(p)).props('flat dense no-caps size=sm') ui.button(part, on_click=lambda p=acc: nav_callback(p)).props('flat dense no-caps size=sm')
if current_path != root_dir: if current_path != root_dir:
ui.space() ui.space()
parent = os.path.dirname(current_path) parent = os.path.dirname(current_path)
@@ -200,23 +200,19 @@ class DeployManager:
def navigate_src(self, path): def navigate_src(self, path):
if os.path.exists(path) and os.path.isdir(path): if os.path.exists(path) and os.path.isdir(path):
self.src_path = path self.src_path = path; self.refresh()
self.refresh()
def navigate_dst(self, path): def navigate_dst(self, path):
if os.path.exists(path) and os.path.isdir(path): if os.path.exists(path) and os.path.isdir(path):
self.dst_path = path self.dst_path = path; self.refresh()
self.refresh()
# --- 6. INTERFACE PRINCIPAL --- # --- 6. UI ---
def refresh(self): def refresh(self):
if self.container: if self.container:
self.container.clear() self.container.clear()
with self.container: with self.container: self.render_layout()
self.render_layout()
def render_layout(self): def render_layout(self):
# TOPBAR: PRESETS
with ui.row().classes('w-full bg-blue-50 p-3 rounded-lg items-center shadow-sm'): with ui.row().classes('w-full bg-blue-50 p-3 rounded-lg items-center shadow-sm'):
ui.icon('bolt', color='blue').classes('text-2xl') ui.icon('bolt', color='blue').classes('text-2xl')
ui.label('SMART DEPLOYS:').classes('font-bold text-blue-900 mr-4') ui.label('SMART DEPLOYS:').classes('font-bold text-blue-900 mr-4')
@@ -224,88 +220,65 @@ class DeployManager:
with ui.button_group().props('rounded'): with ui.button_group().props('rounded'):
ui.button(name, on_click=lambda n=name, p=paths: self.confirm_preset_execution(n, p)).props('color=blue-6') ui.button(name, on_click=lambda n=name, p=paths: self.confirm_preset_execution(n, p)).props('color=blue-6')
ui.button(on_click=lambda n=name: self.delete_preset(n)).props('icon=delete color=red-4') ui.button(on_click=lambda n=name: self.delete_preset(n)).props('icon=delete color=red-4')
ui.button('Salvar Favorito', on_click=self.prompt_save_preset).props('flat icon=add_circle color=green-7').classes('ml-auto') ui.button('Salvar Favorito', on_click=self.prompt_save_preset).props('flat icon=add_circle color=green-7').classes('ml-auto')
# CONTEÚDO: NAVEGADORES
with ui.row().classes('w-full gap-6 mt-4'): with ui.row().classes('w-full gap-6 mt-4'):
# ORIGEM
with ui.column().classes('flex-grow w-1/2'): with ui.column().classes('flex-grow w-1/2'):
ui.label('📂 ORIGEM (Downloads)').classes('text-lg font-bold text-blue-700') ui.label('📂 ORIGEM').classes('text-lg font-bold text-blue-700')
self.render_breadcrumbs(self.src_path, SRC_ROOT, self.navigate_src) self.render_breadcrumbs(self.src_path, SRC_ROOT, self.navigate_src)
self.render_file_list(self.src_path, is_source=True) self.render_file_list(self.src_path, is_source=True)
# DESTINO
with ui.column().classes('flex-grow w-1/2'): with ui.column().classes('flex-grow w-1/2'):
ui.label('🎯 DESTINO (Mídia)').classes('text-lg font-bold text-green-700') ui.label('🎯 DESTINO').classes('text-lg font-bold text-green-700')
self.render_breadcrumbs(self.dst_path, DST_ROOT, self.navigate_dst) self.render_breadcrumbs(self.dst_path, DST_ROOT, self.navigate_dst)
self.render_file_list(self.dst_path, is_source=False) self.render_file_list(self.dst_path, is_source=False)
# SEÇÃO INFERIOR: LOGS E PENDÊNCIAS
with ui.row().classes('w-full gap-6 mt-6'): with ui.row().classes('w-full gap-6 mt-6'):
# PAINEL DE PENDÊNCIAS
with ui.card().classes('flex-grow h-64 bg-orange-50 border-orange-200 shadow-none'): with ui.card().classes('flex-grow h-64 bg-orange-50 border-orange-200 shadow-none'):
with ui.row().classes('w-full items-center border-b pb-2'): with ui.row().classes('w-full items-center border-b pb-2'):
ui.label(f'⚠️ Pendências ({len(self.pendencies)})').classes('font-bold text-orange-900 text-lg') ui.label(f'⚠️ Pendências ({len(self.pendencies)})').classes('font-bold text-orange-900 text-lg')
if self.pendencies: if self.pendencies:
ui.button('SUBSTITUIR TODOS', on_click=lambda: self.handle_all_pendencies('replace')).props('color=green-8 size=sm icon=done_all') ui.button('SUBSTITUIR TODOS', on_click=lambda: self.handle_all_pendencies('replace')).props('color=green-8 size=sm icon=done_all')
ui.button('IGNORAR TODOS', on_click=lambda: self.handle_all_pendencies('ignore')).props('color=grey-7 size=sm icon=clear_all') ui.button('IGNORAR TODOS', on_click=lambda: self.handle_all_pendencies('ignore')).props('color=grey-7 size=sm icon=clear_all')
with ui.scroll_area().classes('w-full h-full'): with ui.scroll_area().classes('w-full h-full'):
for i, p in enumerate(self.pendencies): for i, p in enumerate(self.pendencies):
with ui.row().classes('w-full items-center p-2 border-b bg-white rounded mb-1'): with ui.row().classes('w-full items-center p-2 border-b bg-white rounded mb-1'):
ui.label(p['name']).classes('flex-grow text-xs font-medium') ui.label(p['name']).classes('flex-grow text-xs font-medium')
ui.button(icon='swap_horiz', on_click=lambda idx=i: self.handle_pendency(idx, 'replace')).props('flat dense color=green').tooltip('Substituir') ui.button(icon='swap_horiz', on_click=lambda idx=i: self.handle_pendency(idx, 'replace')).props('flat dense color=green')
ui.button(icon='close', on_click=lambda idx=i: self.handle_pendency(idx, 'ignore')).props('flat dense color=red').tooltip('Manter Original') ui.button(icon='close', on_click=lambda idx=i: self.handle_pendency(idx, 'ignore')).props('flat dense color=red')
# PAINEL DE LOGS
with ui.card().classes('flex-grow h-64 bg-slate-900 text-slate-200 shadow-none'): with ui.card().classes('flex-grow h-64 bg-slate-900 text-slate-200 shadow-none'):
ui.label('📜 Log de Atividades (Persistente)').classes('font-bold border-b border-slate-700 w-full pb-2') ui.label('📜 Logs').classes('font-bold border-b border-slate-700 w-full pb-2')
with ui.scroll_area().classes('w-full h-full'): with ui.scroll_area().classes('w-full h-full'):
# Renderiza os logs carregados
for log in self.logs: for log in self.logs:
# Pinta de vermelho se tiver erro, verde se sucesso color = 'text-red-400' if '' in log else 'text-green-400' if '' in log else 'text-slate-300'
color_cls = 'text-red-400' if '' in log else 'text-green-400' if '' in log else 'text-slate-300' ui.label(f"> {log}").classes(f'text-[10px] font-mono leading-tight {color}')
ui.label(f"> {log}").classes(f'text-[10px] font-mono leading-tight {color_cls}')
# BOTÃO GLOBAL ui.button('INICIAR MOVIMENTAÇÃO', on_click=lambda: self.move_process(self.selected_items, self.dst_path))\
ui.button('INICIAR MOVIMENTAÇÃO DOS SELECIONADOS', on_click=lambda: self.move_process(self.selected_items, self.dst_path))\
.classes('w-full py-6 mt-4 text-xl font-black shadow-lg')\ .classes('w-full py-6 mt-4 text-xl font-black shadow-lg')\
.props('color=green-7 icon=forward')\ .props('color=green-7 icon=forward')\
.bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0) .bind_enabled_from(self, 'selected_items', backward=lambda x: len(x) > 0)
# --- AUXILIARES ---
def render_file_list(self, path, is_source): def render_file_list(self, path, is_source):
try: try:
entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower())) entries = sorted(os.scandir(path), key=lambda e: (not e.is_dir(), e.name.lower()))
with ui.scroll_area().classes('h-[400px] border-2 rounded-lg bg-white w-full shadow-inner'): with ui.scroll_area().classes('h-[400px] border-2 rounded-lg bg-white w-full shadow-inner'):
if not entries: if not entries: ui.label('Pasta vazia').classes('p-4 text-gray-400 italic')
ui.label('Pasta vazia').classes('p-4 text-gray-400 italic')
for entry in entries: for entry in entries:
is_selected = entry.path in self.selected_items is_selected = entry.path in self.selected_items
bg = "bg-blue-100 border-blue-200" if is_selected else "hover:bg-gray-50 border-gray-100" bg = "bg-blue-100 border-blue-200" if is_selected else "hover:bg-gray-50 border-gray-100"
with ui.row().classes(f'w-full p-2 border-b items-center {bg} transition-colors cursor-pointer') as r: with ui.row().classes(f'w-full p-2 border-b items-center {bg} transition-colors cursor-pointer') as r:
if is_source: if is_source: ui.checkbox(value=is_selected, on_change=lambda e, p=entry.path: self.toggle_selection(p)).props('dense')
ui.checkbox(value=is_selected, on_change=lambda e, p=entry.path: self.toggle_selection(p)).props('dense')
icon = 'folder' if entry.is_dir() else 'movie' if entry.name.lower().endswith(('.mkv','.mp4')) else 'description' icon = 'folder' if entry.is_dir() else 'movie' if entry.name.lower().endswith(('.mkv','.mp4')) else 'description'
ui.icon(icon, color='amber-500' if entry.is_dir() else 'blue-grey-400') ui.icon(icon, color='amber-500' if entry.is_dir() else 'blue-grey-400')
ui.label(entry.name).classes('text-sm flex-grow truncate select-none')
lbl = ui.label(entry.name).classes('text-sm flex-grow truncate select-none') if entry.is_dir(): r.on('click', lambda p=entry.path: self.navigate_src(p) if is_source else self.navigate_dst(p))
if entry.is_dir(): elif is_source: r.on('click', lambda p=entry.path: self.toggle_selection(p))
r.on('click', lambda p=entry.path: self.navigate_src(p) if is_source else self.navigate_dst(p)) except: ui.label('Erro ao acessar diretório.').classes('text-red-500 p-4 font-bold')
elif is_source:
r.on('click', lambda p=entry.path: self.toggle_selection(p))
except Exception:
ui.label('Erro ao acessar diretório.').classes('text-red-500 p-4 font-bold')
def prompt_save_preset(self): def prompt_save_preset(self):
with ui.dialog() as d, ui.card().classes('p-6'): with ui.dialog() as d, ui.card().classes('p-6'):
ui.label('Criar Novo Smart Deploy').classes('text-lg font-bold') ui.label('Criar Preset').classes('text-lg font-bold')
ui.label(f'Origem: {self.src_path}').classes('text-xs text-gray-500') name_input = ui.input('Nome')
ui.label(f'Destino: {self.dst_path}').classes('text-xs text-gray-500')
name_input = ui.input('Nome do Atalho (ex: Filmes, 4K, Séries)')
with ui.row().classes('w-full justify-end mt-4'): with ui.row().classes('w-full justify-end mt-4'):
ui.button('Cancelar', on_click=d.close).props('flat') ui.button('Cancelar', on_click=d.close).props('flat')
ui.button('SALVAR', on_click=lambda: [self.save_preset(name_input.value), d.close()]).props('color=green') ui.button('SALVAR', on_click=lambda: [self.save_preset(name_input.value), d.close()]).props('color=green')

View File

@@ -56,3 +56,12 @@ OSError: [Errno 107] Transport endpoint is not connected: '/media/Jellyfin/onedr
[2026-02-05 13:23:13] ✅ Movido: Episódio 07.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:13] ⚠️ Pendência: Episódio 14.mp4
[2026-02-05 13:23:27] 🔄 Substituído: Episódio 14.mp4 [2026-02-05 13:23:27] 🔄 Substituído: Episódio 14.mp4
[2026-02-06 00:20:02] ✅ Movido: 21 SEXTURY - Hot Babe Veronica Leal Gets The Hottest Ass Creampie EVER After Hard Anal Sex.mp4
[2026-02-06 05:16:26] ✅ Movido: TUSHY Abella Danger and Lena Paul Dominate Her Boyfriend and Get Gaped.mp4
[2026-02-06 05:16:27] ✅ Movido: 21 NATURALS - Hot Redhead Veronica Leal Wants Her Tight Ass Fucked.mp4
[2026-02-08 00:51:31] ⚠️ Pendência: Jujutsu Kaisen
[2026-02-08 00:52:15] 🔄 Substituído: Jujutsu Kaisen
[2026-02-08 20:47:28] ✅ Movido: Step Brother cums too early on sisters Mouth and Swallows Cum.mp4
[2026-02-08 20:47:38] ✅ Movido: Horny Couple gets caught in Public Restroom making out.mp4
[2026-02-08 22:31:46] ✅ Movido: Frieren e a Jornada para o Além
[2026-02-08 22:44:01] ✅ Arquivo movido: Episódio 05.mkv