import os
import time
import threading
import random
import subprocess
import shutil
import pickle
import sqlite3
from datetime import datetime, timedelta, timezone
from flask import Flask, request, jsonify, render_template_string
from flask_cors import CORS
import cv2
from pydub import AudioSegment
from pydub.silence import detect_nonsilent
import numpy as np
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
import pytz

DB_PATH = r"C:\Users\jonathan\Desktop\FINANCE CHANEEL\monitorador_juntador.db"
HORARIOS_DIA = [5, 6, 7, 8]

DEFAULT_DESCRIPTION = """
In this video, we reveal the shocking truth behind Warren Buffett’s boldest move ever — and what it could mean for the future of the market. Is a crash on the horizon? Don’t miss these crucial insights!
⏱️ Chapters
0:00 - Introduction
0:45 - What Just Happened
2:10 - Deep Dive into the Situation
3:00 - Final Thoughts and Predictions
8:45 - Outro

📢 Subscribe now for more eye-opening financial insights!
#finance #investing #stockmarket #breakingnews #wealth
"""
DEFAULT_TAGS = [
    "Warren Buffett sells biggest stock",
    "Buffett stock sale 2025",
    "Wall Street reaction Buffett",
    "Warren Buffett stock market news",
    "Buffett biggest investment sold",
    "Buffett stock dump explained",
    "Warren Buffett investing",
    "stock market news 2025",
    "Buffett sell off impact",
    "Warren Buffett market strategy",
    "investment advice Buffett",
    "Buffett latest moves",
    "Wall Street freak out Buffett",
    "trump",
    "trump tarrifs"
]

PASTA_CATEGORIAS = r"C:\Users\jonathan\Desktop\AUTLAND\CATEGORIAS DE IMAGENS VIDEO"
AUDIO_FUNDO = r"C:\Users\jonathan\Desktop\AUTLAND\TRILHA SONORA\soundfund.mp3"
PASTA_DESTINO_VIDEOS = r"C:\Users\jonathan\Desktop\FINANCE CHANEEL\VIDEO PARA POSTAR PRONTOS"

# New folders for video and thumbnail random pick
PASTA_VIDEOS_POSTAGEM = r"C:\Users\jonathan\Desktop\AUTLAND\POSTAGEM\VIDEO CARL JUNG"
PASTA_THUMBNAILS = r"C:\Users\jonathan\Desktop\AUTLAND\THUMBNAILS\TUMB CARL"

def init_db():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        CREATE TABLE IF NOT EXISTS videos (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            filename TEXT UNIQUE,
            title TEXT,
            channel TEXT,
            schedule_time TEXT
        )
    ''')
    c.execute('''
        CREATE TABLE IF NOT EXISTS perfis (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            nome TEXT UNIQUE,
            credentials BLOB,
            canal TEXT
        )
    ''')
    conn.commit()
    conn.close()

def get_perfis():
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT id, nome, canal FROM perfis")
    perfis = c.fetchall()
    conn.close()
    return perfis

def add_perfil(nome, credentials_blob, canal):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("INSERT OR REPLACE INTO perfis (nome, credentials, canal) VALUES (?, ?, ?)", (nome, credentials_blob, canal))
    conn.commit()
    conn.close()

def delete_perfil(nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("DELETE FROM perfis WHERE nome=?", (nome,))
    conn.commit()
    conn.close()

def get_canal_por_perfil(nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT canal FROM perfis WHERE nome=?", (nome,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None

def get_title_from_db(filename):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT title FROM videos WHERE filename=?", (filename,))
    row = c.fetchone()
    conn.close()
    return row[0] if row else None

def set_video_entry(filename, title, channel, schedule_time):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute('''
        INSERT OR REPLACE INTO videos (filename, title, channel, schedule_time)
        VALUES (?, ?, ?, ?)
    ''', (filename, title, channel, schedule_time))
    conn.commit()
    conn.close()

def get_scheduled_times_for_channel(channel):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT schedule_time FROM videos WHERE channel=?", (channel,))
    horarios = [row[0] for row in c.fetchall()]
    conn.close()
    return horarios

def load_credentials_from_blob(blob):
    filename_temp = "temp_creds.pickle"
    with open(filename_temp, "wb") as f:
        f.write(blob)
    with open(filename_temp, "rb") as f:
        creds = pickle.load(f)
    os.remove(filename_temp)
    return creds

def get_cred_blob(profile_nome):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT credentials FROM perfis WHERE nome=?", (profile_nome,))
    row = c.fetchone()
    conn.close()
    if row:
        return row[0]
    else:
        raise Exception("Perfil não encontrado")

def upload_video_to_youtube(credentials_blob, title, description, tags, video_path, thumbnail_path, publish_now, publish_at=None, category_id="22"):
    creds = load_credentials_from_blob(credentials_blob)
    youtube = build("youtube", "v3", credentials=creds)

    if not os.path.exists(video_path):
        raise FileNotFoundError(f"Arquivo não encontrado: {video_path}")

    if publish_now:
        privacy_status = "public"
        publish_at_utc = None
    else:
        if not publish_at:
            raise ValueError("publishAt deve ser informado para agendamento")
        dt_local = datetime.fromisoformat(publish_at)
        now_local = datetime.now(dt_local.tzinfo)
        if dt_local <= now_local:
            raise ValueError(f"publishAt ({dt_local.isoformat()}) deve ser no futuro em relação a agora ({now_local.isoformat()})")
        publish_at_utc = dt_local.astimezone(timezone.utc).isoformat()
        privacy_status = "private"

    body = {
        "snippet": {
            "title": title,
            "description": description,
            "tags": tags,
            "categoryId": category_id
        },
        "status": {
            "privacyStatus": privacy_status,
            "publishAt": publish_at_utc
        }
    }

    media = MediaFileUpload(video_path, mimetype="video/mp4", resumable=True, chunksize=10 * 1024 * 1024)

    request = youtube.videos().insert(
        part="snippet,status",
        body=body,
        media_body=media
    )

    response = None
    while response is None:
        status, response = request.next_chunk()
        if status:
            print(f"Upload {int(status.progress() * 100)}%")

    if thumbnail_path and os.path.exists(thumbnail_path):
        youtube.thumbnails().set(
            videoId=response["id"],
            media_body=MediaFileUpload(thumbnail_path)
        ).execute()

    print(f"[INFO] Upload finalizado, videoId: {response['id']}")

    # After upload, delete video and thumbnail from the respective folders
    try:
        if os.path.exists(video_path) and os.path.commonprefix([video_path, PASTA_VIDEOS_POSTAGEM]) == PASTA_VIDEOS_POSTAGEM:
            os.remove(video_path)
            print(f"Vídeo removido: {video_path}")
    except Exception as e:
        print(f"Erro ao remover vídeo: {e}")
    try:
        if thumbnail_path and os.path.exists(thumbnail_path) and os.path.commonprefix([thumbnail_path, PASTA_THUMBNAILS]) == PASTA_THUMBNAILS:
            os.remove(thumbnail_path)
            print(f"Thumbnail removida: {thumbnail_path}")
    except Exception as e:
        print(f"Erro ao remover thumbnail: {e}")

    return response["id"]

def get_next_available_time(profile_name, start_dt=None, num_slots=1):
    canal = get_canal_por_perfil(profile_name)
    horarios_ocupados = get_scheduled_times_for_channel(canal)

    br_tz = pytz.timezone("America/Sao_Paulo")
    agora = datetime.now(br_tz)
    if start_dt:
        if isinstance(start_dt, str):
            try:
                dt = datetime.fromisoformat(start_dt)
                if dt.tzinfo is None:
                    dt = br_tz.localize(dt)
                start_dt = dt
            except Exception:
                start_dt = None
        elif isinstance(start_dt, datetime):
            if start_dt.tzinfo is None:
                start_dt = br_tz.localize(start_dt)
    if start_dt is None or start_dt <= agora:
        start_dt = agora + timedelta(minutes=1)
        start_dt = start_dt.replace(minute=0, second=0, microsecond=0) + timedelta(hours=1)

    parsed_ocupados = []
    for h in horarios_ocupados:
        try:
            dt = datetime.fromisoformat(h)
            if dt.tzinfo is None:
                dt = br_tz.localize(dt)
        except Exception:
            continue
        parsed_ocupados.append(dt)

    scheduled_slots = []
    candidate_time = start_dt
    limit_date = agora + timedelta(days=30)

    while len(scheduled_slots) < num_slots and candidate_time <= limit_date:
        conflict = False
        for ocupado in parsed_ocupados:
            # Check conflict with existing scheduled slots
            if abs((candidate_time - ocupado).total_seconds()) < 60:
                conflict = True
                break
        # Also check conflict with already scheduled_slots in this batch
        for scheduled in scheduled_slots:
            if abs((candidate_time - scheduled).total_seconds()) < 60:
                conflict = True
                break
        if not conflict:
            scheduled_slots.append(candidate_time)
            candidate_time += timedelta(hours=1)
        else:
            candidate_time += timedelta(hours=1)

    if len(scheduled_slots) < num_slots:
        raise Exception("Não foi possível agendar todos os vídeos nos próximos 30 dias devido a conflitos de horário.")

    return [slot.isoformat() for slot in scheduled_slots]

def processar_envio(profile_nome, video_path, description, tags, thumbnail_path, publicar_agora=False,
                   notify=False, category_id="22", start_schedule_dt=None, qtd_videos=1, progress_callback=None):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT credentials, canal FROM perfis WHERE nome=?", (profile_nome,))
    row = c.fetchone()
    conn.close()

    if not row:
        print("Perfil não encontrado")
        return False

    credentials_blob, canal = row

    # List of all videos and thumbnails in folders, will generate based on qty videos needed
    videos_all = [f for f in os.listdir(PASTA_VIDEOS_POSTAGEM) if f.lower().endswith(".mp4")]
    thumbs_all = [f for f in os.listdir(PASTA_THUMBNAILS) if f.lower().endswith((".jpg", ".jpeg", ".png"))]

    # Ensuring both folders have files
    if not videos_all or not thumbs_all:
        print("[ERRO] Pasta de vídeos ou thumbnails está vazia.")
        return False

    # Determine "Real number" minimal count
    real_number = min(len(videos_all), len(thumbs_all))
    if qtd_videos > real_number:
        print(f"[ERRO] Quantidade de vídeos solicitada ({qtd_videos}) maior que possibilidade real ({real_number}).")
        return False

    # Randomly select qtd_videos files (without replacement to avoid duplication)
    selected_videos = random.sample(videos_all, qtd_videos)
    selected_thumbs = random.sample(thumbs_all, qtd_videos)

    if publicar_agora:
        schedule_times = [None] * qtd_videos
        publish_now_flags = [True] * qtd_videos
    else:
        try:
            schedule_times = get_next_available_time(profile_nome, start_dt=start_schedule_dt, num_slots=qtd_videos)
        except Exception as e:
            print(f"Falha ao calcular horários de agendamento: {e}")
            return False
        publish_now_flags = [False] * qtd_videos

    enviados = 0
    total = qtd_videos

    br_tz = pytz.timezone("America/Sao_Paulo")

    for idx in range(qtd_videos):
        video_file = os.path.join(PASTA_VIDEOS_POSTAGEM, selected_videos[idx])
        thumb_file = os.path.join(PASTA_THUMBNAILS, selected_thumbs[idx])
        publish_now = publish_now_flags[idx]
        publish_at = schedule_times[idx]

        filename_base = os.path.basename(selected_videos[idx])
        filename_sem_ext = os.path.splitext(filename_base)[0]

        titulo_bd = get_title_from_db(filename_sem_ext)
        titulo_final = titulo_bd or filename_sem_ext

        try:
            vid_id = upload_video_to_youtube(
                credentials_blob,
                titulo_final,
                description,
                tags,
                video_file,
                thumb_file,
                publish_now,
                publish_at,
                category_id=category_id
            )
            set_video_entry(filename_sem_ext, titulo_final, canal,
                            publish_at if publish_at else datetime.now(br_tz).isoformat())
            enviados += 1
            if progress_callback:
                progress_callback(enviados, total)
        except Exception as e:
            print(f"Falha no upload do vídeo {filename_base}: {e}")
            return False

    if notify:
        print(f"{enviados} vídeo(s) enviado(s) com sucesso." if publicar_agora else "Vídeos agendados.")
    return True

def zoom_effect(image, progress, zoom_in=True, direction="center"):
    h, w = image.shape[:2]
    zoom_strength = 0.05
    scale = 1 + (zoom_strength * progress) if zoom_in else 1 + (zoom_strength * (1 - progress))
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    if direction == "top":
        x, y = (new_w - w) // 2, 0
    elif direction == "bottom":
        x, y = (new_w - w) // 2, new_h - h
    elif direction == "left":
        x, y = 0, (new_h - h) // 2
    elif direction == "right":
        x, y = new_w - w, (new_h - h) // 2
    elif direction == "top_left":
        x, y = 0, 0
    elif direction == "top_right":
        x, y = new_w - w, 0
    elif direction == "bottom_left":
        x, y = 0, new_h - h
    elif direction == "bottom_right":
        x, y = new_w - w, new_h - h
    else:
        x, y = (new_w - w) // 2, (new_h - h) // 2
    x = max(min(x, new_w - w), 0)
    y = max(min(y, new_h - h), 0)
    cropped = resized[y:y + h, x:x + w]
    return cropped

def move_effect(image, progress, direction="left_to_right"):
    h, w = image.shape[:2]
    dx = int(w * 0.05 * progress)
    dy = int(h * 0.05 * progress)
    if direction == "left_to_right":
        M = np.float32([[1, 0, dx], [0, 1, 0]])
    elif direction == "right_to_left":
        M = np.float32([[1, 0, -dx], [0, 1, 0]])
    elif direction == "top_to_bottom":
        M = np.float32([[1, 0, 0], [0, 1, dy]])
    elif direction == "bottom_to_top":
        M = np.float32([[1, 0, 0], [0, 1, -dy]])
    else:
        return image
    moved = cv2.warpAffine(image, M, (w, h))
    return moved

def ken_burns_effect(image, progress, zoom_in=True, direction="left_to_right"):
    h, w = image.shape[:2]
    zoom_strength = 0.1
    scale = 1 + (zoom_strength * progress) if zoom_in else 1 + (zoom_strength * (1 - progress))
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    dx = int((new_w - w) * progress)
    dy = int((new_h - h) * progress)
    if direction == "left_to_right":
        x, y = dx, 0
    elif direction == "right_to_left":
        x, y = (new_w - w) - dx, 0
    elif direction == "top_to_bottom":
        x, y = 0, dy
    elif direction == "bottom_to_top":
        x, y = 0, (new_h - h) - dy
    elif direction == "top_left_to_bottom_right":
        x, y = dx, dy
    elif direction == "bottom_right_to_top_left":
        x, y = (new_w - w) - dx, (new_h - h) - dy
    else:
        x, y = (new_w - w) // 2, (new_h - h) // 2
    x = max(min(x, new_w - w), 0)
    y = max(min(y, new_h - h), 0)
    cropped = resized[y:y + h, x:x + w]
    return cropped

def fast_zoom_blur_effect(image, progress):
    h, w = image.shape[:2]
    scale = 1 + 0.4 * progress
    new_w, new_h = int(w * scale), int(h * scale)
    resized = cv2.resize(image, (new_w, new_h))
    x = (new_w - w) // 2
    y = (new_h - h) // 2
    zoomed = resized[y:y + h, x:x + w]
    blur_amount = int(progress * 7) * 2 + 1
    if blur_amount % 2 == 0:
        blur_amount += 1
    if blur_amount > 1:
        zoomed = cv2.GaussianBlur(zoomed, (blur_amount, blur_amount), 0)
    return zoomed

def morph_transition_effect(img1, img2, alpha):
    h, w = img1.shape[:2]
    img1 = cv2.resize(img1, (w, h))
    img2 = cv2.resize(img2, (w, h))
    blended = cv2.addWeighted(img1, 1 - alpha, img2, alpha, 0)
    return blended

def slide_effect(image, progress, direction="left_to_right"):
    h, w = image.shape[:2]
    dx = int(w * progress)
    dy = int(h * progress)
    if direction == "left_to_right":
        M = np.float32([[1, 0, dx], [0, 1, 0]])
    elif direction == "right_to_left":
        M = np.float32([[1, 0, -dx], [0, 1, 0]])
    elif direction == "top_to_bottom":
        M = np.float32([[1, 0, 0], [0, 1, dy]])
    elif direction == "bottom_to_top":
        M = np.float32([[1, 0, 0], [0, 1, -dy]])
    else:
        return image
    moved = cv2.warpAffine(image, M, (w, h))
    return moved

def gallery_slide_effect(image, progress):
    h, w = image.shape[:2]
    dx = int(w * 0.3 * progress)
    M = np.float32([[1, 0, dx], [0, 1, 0]])
    shifted = cv2.warpAffine(image, M, (w, h))
    fade_width = int(w * 0.1)
    mask = np.ones((h, w), dtype=np.float32)
    for x_ in range(fade_width):
        alpha = x_ / fade_width
        mask[:, x_] *= alpha
        mask[:, w - 1 - x_] *= alpha
    if shifted.ndim == 3:
        for c in range(3):
            shifted[:, :, c] = shifted[:, :, c].astype(np.float32) * mask
        shifted = shifted.astype(np.uint8)
    else:
        shifted = (shifted.astype(np.float32) * mask).astype(np.uint8)
    return shifted

def load_images_from_folder(folder):
    images = []
    for filename in os.listdir(folder):
        if filename.lower().endswith((".png", ".jpg", ".jpeg")):
            path = os.path.join(folder, filename)
            img = cv2.imread(path)
            if img is not None and img.shape[0] > 0 and img.shape[1] > 0:
                images.append(img)
    random.shuffle(images)
    return images

def get_beat_times(audio_path, frame_ms=50, energy_threshold_factor=1.3):
    audio = AudioSegment.from_file(audio_path).set_channels(1)
    frame_length = frame_ms
    n_frames = len(audio) // frame_length
    energies = []
    for i in range(n_frames):
        frame = audio[i * frame_length: (i + 1) * frame_length]
        energies.append(frame.rms)
    energies = np.array(energies)
    avg_energy = np.mean(energies)
    beat_indices = []
    for i in range(1, len(energies) - 1):
        if energies[i] > energy_threshold_factor * avg_energy and energies[i] > energies[i - 1] and energies[i] > energies[i + 1]:
            beat_indices.append(i)
    beat_times = [i * frame_length / 1000.0 for i in beat_indices]
    filtered_beats = []
    min_separation = 1.0
    for t in beat_times:
        if len(filtered_beats) == 0 or (t - filtered_beats[-1]) > min_separation:
            filtered_beats.append(t)
    return filtered_beats

def remover_silencio(audio_segment, silence_thresh=-50, min_silence_len=500, padding=300):
    nao_silencioso = detect_nonsilent(audio_segment, min_silence_len=min_silence_len, silence_thresh=silence_thresh)
    if not nao_silencioso:
        return audio_segment
    partes = []
    for start, end in nao_silencioso:
        start_pad = max(0, start - padding)
        end_pad = min(len(audio_segment), end + padding)
        partes.append(audio_segment[start_pad:end_pad])
    audio_sem_silencio = partes[0]
    for part in partes[1:]:
        audio_sem_silencio += AudioSegment.silent(duration=150) + part
    return audio_sem_silencio

def create_video_only(video_path, duration, images, fps=30, progress_callback=None, change_times=None):
    if not images:
        raise ValueError("Lista de imagens vazia.")
    height, width, _ = images[0].shape
    total_frames = int(duration * fps)
    out = cv2.VideoWriter(video_path, cv2.VideoWriter_fourcc(*"mp4v"), fps, (width, height))
    img_index = 0
    efeitos = {
        "zoom": 0.15, "move": 0.15, "ken_burns": 0.15, "fast_zoom_blur": 0.10,
        "morph_transition": 0.10, "slide": 0.10, "gallery_slide": 0.10, "static": 0.15,
    }
    efeito_usado_count = {key: 0 for key in efeitos.keys()}
    def escolher_efeito(efeitos, efeito_usado_count):
        chances_ajustadas = {}
        for e, chance in efeitos.items():
            mult = 0.8 ** efeito_usado_count[e]
            chances_ajustadas[e] = chance * mult
        total = sum(chances_ajustadas.values())
        probs = [v / total for v in chances_ajustadas.values()]
        escolha = random.choices(list(chances_ajustadas.keys()), weights=probs, k=1)[0]
        efeito_usado_count[escolha] += 1
        return escolha
    morph_duration_frames = int(fps * 0.5)
    rapid_change_count = 0
    rapid_change_max = 7
    rapid_change_interval_frames = fps
    morph_active = False
    morph_frame = 0
    morph_from_img = None
    morph_to_img = None
    frames_per_image = int(3 * fps)
    def random_interval_frames():
        return int(random.uniform(2.5, 4.0) * fps)
    if change_times is None or len(change_times) == 0:
        frames_per_image = random_interval_frames()
        next_change_frame = frames_per_image
    else:
        change_frames = [int(t * fps) for t in change_times if t * fps < total_frames]
        if change_frames[-1] < total_frames - 1:
            change_frames.append(total_frames - 1)
        change_index = 0
        next_change_frame = change_frames[change_index]
    start_time = time.time()
    current_effect = None
    current_direction_zoom = None
    current_direction_move = None
    current_direction_ken = None
    current_direction_slide = None
    current_direction_static = None
    for i in range(total_frames):
        if rapid_change_count < rapid_change_max:
            if i % rapid_change_interval_frames == 0 and i != 0:
                img_index = (img_index + 1) % len(images)
                rapid_change_count += 1
                current_effect = None
                current_direction_zoom = None
                current_direction_move = None
                current_direction_ken = None
                current_direction_slide = None
                current_direction_static = None
        else:
            if change_times is None or len(change_times) == 0:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    frames_per_image = random_interval_frames()
                    next_change_frame = i + frames_per_image
                    current_effect = None
                    current_direction_zoom = None
                    current_direction_move = None
                    current_direction_ken = None
                    current_direction_slide = None
                    current_direction_static = None
            else:
                if i >= next_change_frame:
                    img_index = (img_index + 1) % len(images)
                    change_index += 1
                    if change_index < len(change_frames):
                        next_change_frame = change_frames[change_index]
                    else:
                        next_change_frame = total_frames + 1
                    current_effect = None
                    current_direction_zoom = None
                    current_direction_move = None
                    current_direction_ken = None
                    current_direction_slide = None
                    current_direction_static = None
        img = images[img_index]
        if change_times is not None and len(change_times) > 0 and i <= next_change_frame:
            prev_frame_change = change_frames[change_index - 1] if change_index > 0 else 0
            frame_in_image = i - prev_frame_change
            total_frames_for_image = next_change_frame - prev_frame_change
        else:
            total_frames_for_image = frames_per_image
            frame_in_image = i % frames_per_image
        progress = frame_in_image / total_frames_for_image if total_frames_for_image > 0 else 0
        progress = min(max(progress, 0), 1)
        if morph_active:
            if morph_frame < morph_duration_frames:
                frame = morph_transition_effect(morph_from_img, morph_to_img, morph_frame / morph_duration_frames)
                morph_frame += 1
                frame = move_effect(frame, progress, direction="left_to_right")
                frame = cv2.resize(frame, (width, height))
                out.write(frame)
                if progress_callback:
                    elapsed = time.time() - start_time
                    frames_done = i + 1
                    frames_left = total_frames - frames_done
                    eta = (elapsed / frames_done) * frames_left if frames_done > 0 else 0
                    progress_callback(frames_done, total_frames, eta)
                continue
            else:
                morph_active = False
                morph_frame = 0
                current_effect = None
        aplicar_morph = False
        if frame_in_image == 0 and random.random() < 0.2 and len(images) > 1:
            morph_active = True
            morph_frame = 0
            morph_from_img = img
            prox_img_index = (img_index + 1) % len(images)
            morph_to_img = images[prox_img_index]
            aplicar_morph = True
        if aplicar_morph:
            continue
        if current_effect is None:
            current_effect = escolher_efeito(efeitos, efeito_usado_count)
        if current_effect == "zoom":
            if current_direction_zoom is None:
                current_direction_zoom = random.choice(["center", "top", "bottom", "left", "right", "top_left", "top_right", "bottom_left", "bottom_right"])
            frame_effect = zoom_effect(img, progress, zoom_in=True, direction=current_direction_zoom)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "move":
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(img, progress, direction=current_direction_move)
        elif current_effect == "ken_burns":
            if current_direction_ken is None:
                current_direction_ken = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top", "top_left_to_bottom_right", "bottom_right_to_top_left"])
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame_effect = ken_burns_effect(img, progress, zoom_in=True, direction=current_direction_ken)
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "fast_zoom_blur":
            frame_effect = fast_zoom_blur_effect(img, progress)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "slide":
            if current_direction_slide is None:
                current_direction_slide = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame_effect = slide_effect(img, progress, direction=current_direction_slide)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "gallery_slide":
            frame_effect = gallery_slide_effect(img, progress)
            if current_direction_move is None:
                current_direction_move = random.choice(["left_to_right", "right_to_left", "top_to_bottom", "bottom_to_top"])
            frame = move_effect(frame_effect, progress, direction=current_direction_move)
        elif current_effect == "static":
            if current_direction_move is None:
                current_direction_move = "left_to_right"
            frame = move_effect(img, progress, direction=current_direction_move)
        else:
            frame = img.copy()
        frame = cv2.resize(frame, (width, height))
        out.write(frame)
        if progress_callback:
            elapsed = time.time() - start_time
            frames_done = i + 1
            frames_left = total_frames - frames_done
            eta = (elapsed / frames_done) * frames_left if frames_done > 0 else 0
            progress_callback(frames_done, total_frames, eta)
    out.release()

def merge_audio_with_video(video_path, audio_path, output_path):
    audio1 = AudioSegment.from_file(audio_path).set_channels(2)
    audio2 = AudioSegment.from_file(AUDIO_FUNDO).set_channels(2)
    audio2 = audio2 - 1
    if len(audio2) < len(audio1):
        audio2 = audio2 * (len(audio1) // len(audio2) + 1)
    audio2 = audio2[:len(audio1)]
    combined = audio1.overlay(audio2)
    temp_audio_path = "combined_temp_audio.mp3"
    combined.export(temp_audio_path, format="mp3")
    command = [
        "ffmpeg",
        "-y",
        "-i", video_path,
        "-i", temp_audio_path,
        "-c:v", "copy",
        "-c:a", "aac",
        "-shortest",
        output_path
    ]
    subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
    os.remove(temp_audio_path)

def create_video_with_audio(audio_path, images_folder, progress_callback=None):
    try:
        audio = AudioSegment.from_file(audio_path)
    except Exception as e:
        print(f"[ERRO] Falha ao carregar áudio: {e}")
        return
    silence_threshold = audio.dBFS - 14
    audio_sem_silencio = remover_silencio(audio, silence_thresh=silence_threshold, min_silence_len=400, padding=300)
    temp_audio_no_silencio = "temp_audio_no_silencio.mp3"
    audio_sem_silencio.export(temp_audio_no_silencio, format="mp3")
    duration_seconds = audio_sem_silencio.duration_seconds
    audio_name = os.path.splitext(os.path.basename(audio_path))[0]
    temp_video_path = f"{audio_name}_temp.mp4"
    final_video_path = os.path.join(PASTA_DESTINO_VIDEOS, f"{audio_name}.mp4")
    images = load_images_from_folder(images_folder)
    if not images:
        print("[ERRO] Nenhuma imagem válida encontrada.")
        return
    try:
        change_times = get_beat_times(temp_audio_no_silencio)
        def internal_progress(current_frame, total_frames, eta):
            if progress_callback:
                percent = int((current_frame / total_frames) * 100)
                eta_str = time.strftime("%M:%S", time.gmtime(max(0, eta)))
                progress_callback(percent, eta_str)
        create_video_only(temp_video_path, duration_seconds, images, fps=30, progress_callback=internal_progress, change_times=change_times)
        merge_audio_with_video(temp_video_path, temp_audio_no_silencio, final_video_path)
        os.remove(temp_video_path)
        os.remove(temp_audio_no_silencio)
        if progress_callback:
            progress_callback(100, "00:00")
    except Exception as e:
        print(f"[ERRO FATAL] Falha ao criar vídeo: {e}")

def create_video_with_audio_multiple(audio_paths, images_folder, progress_callback=None):
    total = len(audio_paths)
    completed = 0
    for audio_path in audio_paths:
        def local_progress(percent, eta):
            if progress_callback:
                progress_callback(completed, total, percent, eta)
        create_video_with_audio(audio_path, images_folder, progress_callback=local_progress)
        completed += 1
    if progress_callback:
        progress_callback(total, total, 100, "00:00")


app = Flask(__name__)
CORS(app)

# Util functions to count files in specified folders - for interface live counts

def count_video_files():
    if not os.path.isdir(PASTA_VIDEOS_POSTAGEM):
        return 0
    return len([f for f in os.listdir(PASTA_VIDEOS_POSTAGEM) if f.lower().endswith(".mp4")])

def count_thumbnail_files():
    if not os.path.isdir(PASTA_THUMBNAILS):
        return 0
    return len([f for f in os.listdir(PASTA_THUMBNAILS) if f.lower().endswith((".jpg", ".jpeg", ".png"))])

@app.route("/")
def home():
    html = """
<!doctype html>
<html lang="pt-br">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Carl Jung Controle Web - Profissional</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <style>
      body { background-color:#343a40; color:white; }
      .card { background-color:#23272b; }
      .progress { height: 25px; }
      .footer-stats { font-size: 0.9rem; text-align: center; margin-top: 10px; color:#bbb; }
      .status-box { height: 150px; overflow-y: auto; white-space: pre-wrap; font-family: monospace; background-color: #1a1d21; padding: 10px; border-radius: 6px; }
    </style>
  </head>
  <body>
    <div class="container py-4">
      <h1 class="mb-4 text-center">Carl Jung - Controle Web Profissional</h1>
      <div class="row gx-5">
        <!-- Editor Automático de Vídeo -->
        <div class="col-lg-6 mb-4">
          <div class="card shadow">
            <div class="card-header text-center">
              <h2>Editor Automático de Vídeo</h2>
            </div>
            <div class="card-body">
              <div class="mb-3">
                <label for="categoria" class="form-label">Categoria de Imagens</label>
                <select id="categoria" class="form-select"></select>
              </div>
              <div class="mb-3">
                <label for="audios" class="form-label">Áudios (caminhos separados por vírgula)</label>
                <textarea id="audios" class="form-control" rows="5" placeholder="C:\\audio1.mp3, C:\\audio2.mp3"></textarea>
              </div>
              <button class="btn btn-success w-100" onclick="startEditing()">Iniciar Edição</button>
              <div class="mt-3">
                <div class="progress" style="height: 25px;">
                  <div id="progressBarEditor" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div>
                </div>
                <div class="footer-stats" id="etaEditor">Tempo Restante: --:--</div>
              </div>
              <pre id="status" class="status-box mt-3">Nenhuma ação iniciada.</pre>
              <div class="mt-3 text-center">
                <strong>Arquivos disponíveis:</strong><br>
                Vídeos: <span id="countVideos">0</span><br>
                Thumbnails: <span id="countThumbs">0</span><br>
                Real number (mínimo): <span id="countReal">0</span>
              </div>
            </div>
          </div>
        </div>
        <!-- Agendador e Upload YouTube -->
        <div class="col-lg-6 mb-4">
          <div class="card shadow">
            <div class="card-header text-center">
              <h2>Agendador e Upload YouTube</h2>
            </div>
            <div class="card-body">
              <div class="mb-3">
                <label class="form-label">Video</label>
                <input type="text" id="video_path" class="form-control" placeholder="Selecionado aleatoriamente da pasta; não preencha!" disabled>
              </div>
              <div class="mb-3">
                <label class="form-label">Thumbnail</label>
                <input type="text" id="thumb_path" class="form-control" placeholder="Selecionada aleatoriamente da pasta; não preencha!" disabled>
              </div>
              <div class="mb-3">
                <label for="perfil" class="form-label">Perfil</label>
                <select id="perfil" class="form-select"></select>
              </div>
              <div class="mb-3">
                <label for="descricao" class="form-label">Descrição</label>
                <textarea id="descricao" class="form-control" rows="5">""" + DEFAULT_DESCRIPTION + """</textarea>
              </div>
              <div class="mb-3">
                <label for="tags" class="form-label">Tags (separadas por vírgula)</label>
                <input type="text" id="tags" class="form-control" value='""" + ",".join(DEFAULT_TAGS) + """'>
              </div>
              <div class="mb-3 row gx-2 align-items-end">
                <div class="col">
                  <label for="num_videos" class="form-label">Número de vídeos</label>
                  <input type="number" id="num_videos" class="form-control" value="1" min="1">
                </div>
                <div class="col">
                  <label for="data" class="form-label">Data (AAAA-MM-DD)</label>
                  <input type="date" id="data" class="form-control">
                </div>
                <div class="col">
                  <label for="hora" class="form-label">Hora (HH:MM)</label>
                  <input type="time" id="hora" class="form-control" value="06:00">
                </div>
              </div>
              <button class="btn btn-primary w-100 mt-3" onclick="startUpload()">Enviar Vídeos</button>
              <div class="mt-3">
                <div class="progress" style="height: 25px;">
                  <div id="progressBarUpload" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%">0%</div>
                </div>
                <div class="footer-stats" id="etaUpload">Tempo Restante: --:--</div>
              </div>
              <pre id="status_upload" class="status-box mt-3">Nenhuma ação iniciada.</pre>
              <div class="mt-3 text-center">
                <strong>Arquivos disponíveis para upload:</strong><br>
                Vídeos: <span id="countVideosUpload">0</span><br>
                Thumbnails: <span id="countThumbsUpload">0</span><br>
                Real number (mínimo): <span id="countRealUpload">0</span>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
    <script>
      async function carregarCategorias(){
        const resp = await fetch('/categorias');
        const dados = await resp.json();
        const sel = document.getElementById('categoria');
        sel.innerHTML = '';
        dados.categorias.forEach(cat => {
          const opt = document.createElement('option');
          opt.value = cat;
          opt.textContent = cat;
          sel.appendChild(opt);
        });
      }

      async function carregarPerfis(){
        const resp = await fetch('/perfis');
        const dados = await resp.json();
        const sel = document.getElementById('perfil');
        sel.innerHTML = '';
        dados.perfis.forEach(p => {
          const opt = document.createElement('option');
          opt.value = p.nome;
          opt.textContent = p.nome;
          sel.appendChild(opt);
        });
      }

      async function updateFileCounts(){
        try {
          const resp = await fetch('/file_counts');
          const data = await resp.json();
          document.getElementById('countVideos').textContent = data.videos;
          document.getElementById('countThumbs').textContent = data.thumbs;
          document.getElementById('countReal').textContent = data.real;
          document.getElementById('countVideosUpload').textContent = data.videos;
          document.getElementById('countThumbsUpload').textContent = data.thumbs;
          document.getElementById('countRealUpload').textContent = data.real;
        } catch (error) {
          console.error('Erro ao atualizar contadores:', error);
        }
      }

      let editingInterval = null;
      let uploadInterval = null;

      async function startEditing(){
        const audiosRaw = document.getElementById('audios').value;
        const audios = audiosRaw.split(',').map(s => s.trim()).filter(s => s.length > 0);
        const categoria = document.getElementById('categoria').value;
        if(!audios.length || !categoria){
          alert("Informe áudios e selecione categoria.");
          return;
        }
        const status = document.getElementById('status');
        const progressBar = document.getElementById('progressBarEditor');
        const etaText = document.getElementById('etaEditor');

        progressBar.style.width = '0%';
        progressBar.textContent = '0%';
        etaText.textContent = 'Tempo Restante: --:--';
        status.textContent = 'Iniciando...';

        try {
          const resp = await fetch('/start_editing', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({audio_paths: audios, category: categoria})
          });
          const data = await resp.json();
          status.textContent = data.message || 'Iniciado.';
          // Start polling for progress:
          if(editingInterval) clearInterval(editingInterval);
          editingInterval = setInterval(async () => {
            try {
              const progResp = await fetch('/progress_editing');
              const prog = await progResp.json();
              if(prog.percent !== undefined){
                progressBar.style.width = prog.percent + '%';
                progressBar.textContent = prog.percent + '%';
                etaText.textContent = 'Tempo Restante: ' + prog.eta;
                status.textContent = prog.message;
                if(prog.percent >= 100){
                  clearInterval(editingInterval);
                }
              }
            } catch (e) {
              console.log("Erro no polling do progresso do editor:", e);
            }
          }, 1500);
        } catch(e) {
          status.textContent = 'Erro: ' + e.message;
        }
      }

      async function startUpload(){
        const perfil = document.getElementById('perfil').value;
        const descricao = document.getElementById('descricao').value;
        const tagsRaw = document.getElementById('tags').value;
        const tags = tagsRaw.split(',').map(s => s.trim()).filter(s => s.length > 0);
        const num_videos = parseInt(document.getElementById('num_videos').value);
        const data_ = document.getElementById('data').value;
        const hora = document.getElementById('hora').value;
        const status_upload = document.getElementById('status_upload');
        const progressBar = document.getElementById('progressBarUpload');
        const etaText = document.getElementById('etaUpload');

        if(!perfil || !data_ || !hora || num_videos < 1){
          alert('Preencha todos os campos obrigatórios corretamente.');
          return;
        }
        status_upload.textContent = 'Iniciando upload/agendamento...';
        progressBar.style.width = '0%';
        progressBar.textContent = '0%';
        etaText.textContent = 'Tempo Restante: --:--';

        try {
          const resp = await fetch('/start_upload', {
            method: 'POST',
            headers: {'Content-Type': 'application/json'},
            body: JSON.stringify({
              profile_nome: perfil,
              description: descricao,
              tags: tags,
              qtd_videos: num_videos,
              data: data_,
              hora: hora
            })
          });
          const dataResp = await resp.json();
          status_upload.textContent = dataResp.message || 'Iniciado.';
          // Start polling for progress
          if(uploadInterval) clearInterval(uploadInterval);
          uploadInterval = setInterval(async () => {
            try {
              const progResp = await fetch('/progress_upload');
              const prog = await progResp.json();
              if(prog.percent !== undefined){
                progressBar.style.width = prog.percent + '%';
                progressBar.textContent = prog.percent + '%';
                etaText.textContent = 'Tempo Restante: ' + prog.eta;
                status_upload.textContent = prog.message;
                if(prog.percent >= 100){
                  clearInterval(uploadInterval);
                }
              }
            } catch(e) {
              console.log("Erro no polling do progresso do upload:", e);
            }
          }, 1500);
        } catch(e) {
          status_upload.textContent = 'Erro: ' + e.message;
        }
      }

      window.onload = () => {
        carregarCategorias();
        carregarPerfis();
        updateFileCounts();
        setInterval(updateFileCounts, 5000); // Atualiza a cada 5 segundos
        const hoje = new Date().toISOString().slice(0,10);
        document.getElementById('data').value = hoje;
      }
    </script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
  </body>
</html>"""
    return render_template_string(html)

@app.route("/categorias")
def categorias():
    if not os.path.isdir(PASTA_CATEGORIAS):
        return jsonify({"categorias": []})
    subpastas = [nome for nome in os.listdir(PASTA_CATEGORIAS) if os.path.isdir(os.path.join(PASTA_CATEGORIAS, nome))]
    return jsonify({"categorias": subpastas})

@app.route("/perfis")
def perfis():
    perfis_list = get_perfis()
    perfis_json = [{"id": p[0], "nome": p[1], "canal": p[2]} for p in perfis_list]
    return jsonify({"perfis": perfis_json})

@app.route("/file_counts")
def file_counts():
    videos = count_video_files()
    thumbs = count_thumbnail_files()
    real = min(videos, thumbs)
    return jsonify({"videos": videos, "thumbs": thumbs, "real": real})

# Variables to hold progress state for editor and uploader
progress_editor = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}
progress_upload = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}

@app.route("/progress_editing")
def progress_editing():
    return jsonify(progress_editor)

@app.route("/progress_upload")
def progress_upload_status():
    return jsonify(progress_upload)

# Modified create_video_with_audio_multiple to update progress_editor
def create_video_with_audio_multiple_mod(audio_paths, images_folder):
    total = len(audio_paths)
    completed = 0
    for idx, audio_path in enumerate(audio_paths):
        def local_progress(percent, eta):
            progress_editor["percent"] = int(((completed + percent/100) / total) * 100)
            progress_editor["eta"] = eta
            progress_editor["message"] = f"Processando áudio {completed + 1} de {total} - {progress_editor['percent']}%"
        create_video_with_audio(audio_path, images_folder, progress_callback=local_progress)
        completed += 1
    progress_editor["percent"] = 100
    progress_editor["eta"] = "00:00"
    progress_editor["message"] = f"Processamento concluído para {total} arquivos."

@app.route("/start_editing", methods=["POST"])
def start_editing_api():
    data = request.json
    audio_paths = data.get("audio_paths", [])
    category = data.get("category", "")

    def async_editing():
        try:
            create_video_with_audio_multiple_mod(audio_paths, os.path.join(PASTA_CATEGORIAS, category))
        except Exception as e:
            progress_editor["message"] = f"Erro durante edição: {str(e)}"
            print(f"Erro durante edição: {e}")

    threading.Thread(target=async_editing, daemon=True).start()
    progress_editor["percent"] = 0
    progress_editor["eta"] = "--:--"
    progress_editor["message"] = f"Edição iniciada para {len(audio_paths)} arquivos na categoria {category}."
    return jsonify({"message": f"Edição iniciada para {len(audio_paths)} arquivos na categoria {category}."})

# Modified processar_envio to update progress_upload dict & no longer receive progress_callback param because we handle here.
def processar_envio_mod(profile_nome, qtd_videos, description, tags, category_id, start_schedule_dt):
    conn = sqlite3.connect(DB_PATH)
    c = conn.cursor()
    c.execute("SELECT credentials, canal FROM perfis WHERE nome=?", (profile_nome,))
    row = c.fetchone()
    conn.close()

    if not row:
        progress_upload["message"] = "Perfil não encontrado"
        return False

    credentials_blob, canal = row

    videos_all = [f for f in os.listdir(PASTA_VIDEOS_POSTAGEM) if f.lower().endswith(".mp4")]
    thumbs_all = [f for f in os.listdir(PASTA_THUMBNAILS) if f.lower().endswith((".jpg", ".jpeg", ".png"))]

    if not videos_all or not thumbs_all:
        progress_upload["message"] = "[ERRO] Pasta de vídeos ou thumbnails está vazia."
        return False

    real_number = min(len(videos_all), len(thumbs_all))
    if qtd_videos > real_number:
        progress_upload["message"] = f"[ERRO] Quantidade solicitada ({qtd_videos}) maior que possibilidade real ({real_number})."
        return False

    selected_videos = random.sample(videos_all, qtd_videos)
    selected_thumbs = random.sample(thumbs_all, qtd_videos)

    try:
        schedule_times = get_next_available_time(profile_nome, start_dt=start_schedule_dt, num_slots=qtd_videos)
    except Exception as e:
        progress_upload["message"] = f"Falha ao calcular horários: {str(e)}"
        return False

    enviados = 0
    total = qtd_videos

    br_tz = pytz.timezone("America/Sao_Paulo")

    for idx in range(qtd_videos):
        video_file = os.path.join(PASTA_VIDEOS_POSTAGEM, selected_videos[idx])
        thumb_file = os.path.join(PASTA_THUMBNAILS, selected_thumbs[idx])
        publish_now = False
        publish_at = schedule_times[idx]

        filename_base = os.path.basename(selected_videos[idx])
        filename_sem_ext = os.path.splitext(filename_base)[0]

        titulo_bd = get_title_from_db(filename_sem_ext)
        titulo_final = titulo_bd or filename_sem_ext

        try:
            vid_id = upload_video_to_youtube(
                credentials_blob,
                titulo_final,
                description,
                tags,
                video_file,
                thumb_file,
                publish_now,
                publish_at,
                category_id=category_id
            )
            set_video_entry(filename_sem_ext, titulo_final, canal,
                            publish_at if publish_at else datetime.now(br_tz).isoformat())
            enviados += 1
            progress_upload["percent"] = int((enviados / total) * 100)
            progress_upload["message"] = f"Enviado(s) {enviados} de {total} vídeo(s)."
            # Estimate ETA (simple): assume equal time per video, no precise
            esteta_seconds = ((total - enviados) * 60)  # arbitrarily 1min per video
            mins = esteta_seconds // 60
            secs = esteta_seconds % 60
            progress_upload["eta"] = f"{int(mins):02}:{int(secs):02}"
        except Exception as e:
            progress_upload["message"] = f"Falha no upload do vídeo {filename_base}: {str(e)}"
            return False

    progress_upload["percent"] = 100
    progress_upload["eta"] = "00:00"
    progress_upload["message"] = f"Upload/agendamento concluído para {total} vídeos."
    return True

@app.route("/start_upload", methods=["POST"])
def start_upload_api():
    data = request.json
    profile_nome = data.get("profile_nome", "")
    description = data.get("description", DEFAULT_DESCRIPTION)
    tags = data.get("tags", DEFAULT_TAGS)
    qtd_videos = data.get("qtd_videos", 1)
    data_str = data.get("data", "")
    hora_str = data.get("hora", "")

    try:
        dt_str = f"{data_str}T{hora_str}:00"
        dt_parsed = datetime.fromisoformat(dt_str)
        br_tz = pytz.timezone("America/Sao_Paulo")
        if dt_parsed.tzinfo is None:
            dt_parsed = br_tz.localize(dt_parsed)
        else:
            dt_parsed = dt_parsed.astimezone(br_tz)
        agora = datetime.now(br_tz)
        if dt_parsed <= agora:
            return jsonify({"message": "Data e hora devem ser no futuro"}), 400
    except Exception as e:
        return jsonify({"message": f"Data ou hora inválida: {e}"}), 400

    try:
        qtd_videos = int(qtd_videos)
    except:
        qtd_videos = 1

    def async_upload():
        try:
            processar_envio_mod(profile_nome=profile_nome,
                              qtd_videos=qtd_videos,
                              description=description,
                              tags=tags,
                              category_id="22",
                              start_schedule_dt=dt_parsed)
        except Exception as e:
            print(f"Erro no upload/agendamento: {e}")
            progress_upload["message"] = f"Erro no upload/agendamento: {str(e)}"
            progress_upload["eta"] = "--:--"
            progress_upload["percent"] = 0

    # Reset progress info before start
    progress_upload["percent"] = 0
    progress_upload["eta"] = "--:--"
    progress_upload["message"] = "Upload/agendamento iniciado."

    threading.Thread(target=async_upload, daemon=True).start()

    return jsonify({"message": "Upload/agendamento iniciado"})


if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True) . chat na parte de selecionar audio (Áudios (caminhos separados por vírgula) faça o seguinte troque para pasta. (C:\Users\jonathan\Desktop\AUTLAND\AUDIOS\AUDIOS CARL JUNG) quando clicar iniciar ele pega um audio aleatorio da pasta e começa a edição lembrando assim que terminar totalmente toda edição tudo ele apaga o audio usado . e envia o video e resultado final pronto para pasta. (C:\Users\jonathan\Desktop\AUTLAND\POSTAGEM\VIDEO CARL JUNG)  . ( atenção quando eu clicar iniciar ele fica ativado a assim que terminar ele já começa a editar outro até terminar todos da pasta caso eu clique em parar ele espera terminar o em execução depois de terminado tudo ele para. use as cores de tema marrom , preto e amarelo fosco .  faça uma interface mas bonita com uma qualidade profissional com graficos etc.. adc novas funções e troque o titulo da interface por CARL JUNG AUTMATION. me de o numero em tempo real de quanto arquivos tem na pasta . C:\Users\jonathan\Desktop\AUTLAND\POSTAGEM\VIDEO CARL JUNG . e na pasta C:\Users\jonathan\Desktop\AUTLAND\THUMBNAILS\TUMB CARL . e um chamado Real number onde ele mostra o numero de possibilidade ( isso que dizer se na pasta 1 tem 23 na pasta 2 tem 19 . são 19 videos que são capas de serem feitos pós não tem como mandar video sem thumb e nem mandar thumb sem video então um precisa do outro então esse é o real.) lembrando que deve ficar em uma area da interface essas informações. "Por favor, me dê o código completo e integral da solução, linha por linha, sem resumir ou omitir nada, mesmo que seja um código muito longo. Divida em partes se necessário, mas garanta que todas as funções, classes e trechos estejam presentes na íntegra." não quero comentários do tipo “o restante do código é este ou aquele” — tudo deve ser escrito. chat me de tudo mesmo que o codigo fique gigante e passe do limite faça o codigo completo mesmo que fique longo pfv você tá abreviando partes do codigo me de completo sem abreviar pfv escreva todas as partes linha por linha não tente me enganar . não pessa para minha incluir nem nada escreva você tudo