import os
import time
import threading
import random
import subprocess
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"

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\CARL JUNG"
AUDIO_FUNDO = r"C:\Users\jonathan\Desktop\AUTLAND\TRILHA SONORA\soundfund.mp3"

PASTA_VIDEOS_POSTAGEM = r"C:\Users\jonathan\Desktop\AUTLAND\POSTAGEM\VIDEO CARL JUNG"
PASTA_THUMBNAILS = r"C:\Users\jonathan\Desktop\AUTLAND\THUMBNAILS\TUMB CARL"
PASTA_AUDIOS_CARL = r"C:\Users\jonathan\Desktop\AUTLAND\AUDIOS\AUDIOS CARL JUNG"

lock_editing_process = threading.Lock()
stop_requested = False

progress_editor = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}

progress_upload = {
    "percent": 0,
    "eta": "--:--",
    "message": "Nenhuma ação iniciada."
}

app = Flask(__name__)
CORS(app)

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, credentials FROM perfis")
    perfis = c.fetchall()
    conn.close()
    return perfis

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 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 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']}")

    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_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 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:
            if abs((candidate_time - ocupado).total_seconds()) < 60:
                conflict = True
                break
        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 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 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 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)
        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, 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_VIDEOS_POSTAGEM, f"{audio_name}.mp4")

    images = load_images_from_folder(PASTA_CATEGORIAS)
    if not images:
        print("[ERRO] Nenhuma imagem válida encontrada na pasta fixa CARL JUNG.")
        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_custom(delete_audio_after=True, stop_when_done=True):
    global lock_editing_process, stop_requested
    with lock_editing_process:
        audios_all = [f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))]
        total = len(audios_all)
        completed = 0
        if total == 0:
            print("[INFO] Não há áudios para processar.")
            progress_editor["percent"] = 100
            progress_editor["eta"] = "00:00"
            progress_editor["message"] = "Nenhum áudio para processar."
            return
        while audios_all:
            if stop_requested:
                print("[INFO] Solicitação de parada recebida. Finalizando após o áudio atual.")
                break
            audio_file = random.choice(audios_all)
            audio_path = os.path.join(PASTA_AUDIOS_CARL, audio_file)
            progress_editor["message"] = f"Processando áudio: {audio_file} ({completed+1}/{total})"
            progress_editor["percent"] = int((completed / total) * 100)
            try:
                create_video_with_audio(audio_path, progress_callback=None)
                if delete_audio_after:
                    try:
                        os.remove(audio_path)
                        print(f"[INFO] Áudio removido: {audio_file}")
                    except Exception as e:
                        print(f"[WARN] Não foi possível remover áudio {audio_file}: {e}")
                completed += 1
                audios_all = [f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))]
            except Exception as e:
                print(f"[ERRO] Falha ao processar áudio {audio_file}: {e}")
                audios_all.remove(audio_file)
                completed += 1
        progress_editor["percent"] = 100
        progress_editor["eta"] = "00:00"
        progress_editor["message"] = "Processamento concluído para todos os áudios."
    if stop_when_done:
        stop_requested = False

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"))])

def count_audio_files():
    if not os.path.isdir(PASTA_AUDIOS_CARL):
        return 0
    return len([f for f in os.listdir(PASTA_AUDIOS_CARL) if f.lower().endswith((".mp3", ".wav", ".m4a", ".flac"))])

@app.route("/")
def home():
    perfis_list = get_perfis()
    options_html = ""
    for p in perfis_list:
        id_, nome, canal, credentials = p
        missing_text = "" if credentials else " (Credenciais Ausentes)"
        options_html += f'<option value="{nome}">{nome}{missing_text}</option>'
    html = f"""
<!doctype html>
<html lang="pt-br">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>CARL JUNG AUTOMATION</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" />
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
      body {{
        background-color: #1a120b;
        color: #f6e7b4;
        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
      }}
      .header {{
        text-align: center;
        padding: 25px 10px;
        font-weight: 700;
        font-size: 2.5rem;
        color: #c6a961;
        text-shadow: 1px 1px 3px #3a2e1b;
      }}
      .container {{
        max-width: 1200px;
      }}
      .card {{
        background-color: #2f1e0e;
        border: 2px solid #c6a961;
        border-radius: 12px;
        box-shadow: 0 0 13px #846f33bb;
      }}
      .card-header {{
        background-color: #3f2a11;
        color: #d6bb68;
        font-weight: 700;
        font-size: 1.3rem;
        text-align: center;
      }}
      label {{
        font-weight: 600;
      }}
      button {{
        background-color: #c6a961;
        border: none;
        color: #1a120b;
        font-weight: 700;
        box-shadow: 0 0 8px #c6a961aa;
      }}
      button:hover {{
        background-color: #b59759;
        color: #eae1b0;
      }}
      .progress {{
        height: 28px;
        border-radius: 14px;
        background-color: #3a2f19;
        box-shadow: inset 0 1px 3px #846f3399;
      }}
      .progress-bar {{
        background-color: #c6a961;
      }}
      .status-box {{
        font-family: monospace;
        background: #241907;
        border-radius: 10px;
        padding: 10px;
        height: 140px;
        overflow-y: auto;
        white-space: pre-wrap;
        box-shadow: inset 0 0 10px #c6a961aa;
      }}
      .file-counts {{
        font-size: 0.9rem;
        background: #372a13;
        padding: 15px;
        border-radius: 10px;
        color: #e6dca8;
        margin-top: 10px;
        box-shadow: 0 0 10px #9e8b32aa;
        font-weight: 600;
      }}
      .stop-button {{
        background-color: #b33a1a;
        box-shadow: 0 0 12px #b33a1aaa;
        margin-top: 8px;
      }}
      .stop-button:hover {{
        background-color: #993214;
        color: #fff7d2;
      }}
      .footer {{
        text-align: center;
        margin-top: 25px;
        color: #846f33bb;
      }}
      .chart-container {{
        margin-top: 15px;
      }}
    </style>
  </head>
  <body>
    <div class="header">CARL JUNG AUTOMATION</div>
    <div class="container">
      <div class="row g-4">
        <div class="col-lg-6 col-md-12">
          <div class="card p-3">
            <div class="card-header">Processamento Contínuo de Áudio e Vídeo (Imagens: Pasta Fixa CARL JUNG)</div>
            <div>
              <p>Ao iniciar, um áudio aleatório da pasta será selecionado para criar o vídeo automaticamente. Após geração, o áudio será apagado e o processo continua até terminar todos os áudios da pasta.</p>
              <button id="btnStartEdit" class="w-100 mb-2">Iniciar Processamento Contínuo</button>
              <button id="btnStopEdit" class="w-100 stop-button mb-2" disabled>Parar Após Terminar Atual</button>
              <div class="progress">
                <div class="progress-bar progress-bar-striped progress-bar-animated" id="progressEditor" style="width: 0%">0%</div>
              </div>
              <div class="file-counts mt-3" id="editorStatus">Nenhuma ação iniciada.</div>
            </div>
            <div class="file-counts mt-4">
              <strong>Estatísticas em Tempo Real</strong><br>
              Áudios Pendentes: <span id="countAudios">0</span><br>
              Vídeos (Pasta Postagem): <span id="countVideosPostagem">0</span><br>
              Thumbnails Disponíveis: <span id="countThumbsCarl">0</span><br>
              <em>Real Number:</em> <span id="countReal">0</span>
            </div>
            <div class="chart-container mt-4">
              <canvas id="progressChart" height="150"></canvas>
            </div>
          </div>
        </div>

        <div class="col-lg-6 col-md-12">
          <div class="card p-3">
            <div class="card-header">Agendador e Upload YouTube</div>
            <div>
              <label for="perfilSelect" class="form-label">Selecionar Perfil</label>
              <select id="perfilSelect" class="form-select mb-2">
                {options_html}
              </select>
              <small id="warningCreds" style="color:#d97a00; display:none;">* Alguns perfis não possuem credenciais cadastradas e não funcionarão para upload.</small>
              <label for="descInput" class="form-label">Descrição</label>
              <textarea id="descInput" class="form-control mb-2" rows="5">{DEFAULT_DESCRIPTION.strip()}</textarea>
              <label for="tagsInput" class="form-label">Tags (vírgula separado)</label>
              <input type="text" id="tagsInput" class="form-control mb-2" value='{",".join(DEFAULT_TAGS)}'>
              <div class="row g-2 mb-3">
                <div class="col-6">
                  <label for="numVideos" class="form-label">Número de vídeos</label>
                  <input type="number" id="numVideos" class="form-control" min="1" value="1">
                </div>
                <div class="col-6">
                  <label for="dateUpload" class="form-label">Data Upload/Agendamento</label>
                  <input type="date" id="dateUpload" class="form-control">
                </div>
                <div class="col-6">
                  <label for="timeUpload" class="form-label">Hora (HH:MM)</label>
                  <input type="time" id="timeUpload" class="form-control" value="06:00">
                </div>
              </div>
              <button id="btnStartUpload" class="w-100">Iniciar Upload/Agendamento</button>
              <div class="progress mt-2">
                <div class="progress-bar progress-bar-striped progress-bar-animated" id="progressUpload" role="progressbar" style="width: 0%">0%</div>
              </div>
              <div class="file-counts mt-3" id="uploadStatus">Nenhuma ação iniciada.</div>
              <div class="file-counts mt-3">
                <strong>Arquivos para Upload</strong><br>
                Vídeos: <span id="countVideosUpload">0</span><br>
                Thumbnails: <span id="countThumbnailsUpload">0</span><br>
                <em>Real Number:</em> <span id="countRealUpload">0</span>
              </div>
            </div>
          </div>
        </div>

      </div>
    </div>
    <div class="footer mt-4 text-center text-muted">© 2024 Carl Jung Automation</div>

<script>
  const perfilSelect = document.getElementById("perfilSelect");
  function checkCredentialsWarning() {{
    let missing = false;
    for(let i=0; i<perfilSelect.options.length; i++) {{
      if(perfilSelect.options[i].text.includes("(Credenciais Ausentes)")) {{
        missing = true;
        break;
      }}
    }}
    document.getElementById("warningCreds").style.display = missing ? "block" : "none";
  }}

  let editorProgressPercentHistory = [];
  const maxDataPoints = 30;

  async function fetchFileCounts() {{
    try {{
      const response = await fetch("/file_counts_custom");
      const data = await response.json();
      document.getElementById("countAudios").textContent = data.audios;
      document.getElementById("countVideosPostagem").textContent = data.videos_postagem;
      document.getElementById("countThumbsCarl").textContent = data.thumbs_carl;
      document.getElementById("countReal").textContent = data.real_number;

      document.getElementById("countVideosUpload").textContent = data.videos_postagem;
      document.getElementById("countThumbnailsUpload").textContent = data.thumbs_carl;
      document.getElementById("countRealUpload").textContent = data.real_number;
    }} catch (error) {{
      console.error("Erro ao atualizar contadores:", error);
    }}
  }}

  async function updateEditorProgress() {{
    try {{
      const resp = await fetch("/progress_editing");
      const prog = await resp.json();
      document.getElementById("progressEditor").style.width = prog.percent + "%";
      document.getElementById("progressEditor").textContent = prog.percent + "%";
      document.getElementById("editorStatus").textContent = prog.message + " | Tempo Estimado: " + prog.eta;

      editorProgressPercentHistory.push(prog.percent);
      if(editorProgressPercentHistory.length > maxDataPoints) {{
        editorProgressPercentHistory.shift();
      }}
      if(window.progressChartInstance){{
        window.progressChartInstance.data.datasets[0].data = editorProgressPercentHistory;
        window.progressChartInstance.update();
      }}
    }} catch (e) {{
    }}
  }}

  async function updateUploadProgress() {{
    try {{
      const resp = await fetch("/progress_upload");
      const prog = await resp.json();
      document.getElementById("progressUpload").style.width = prog.percent + "%";
      document.getElementById("progressUpload").textContent = prog.percent + "%";
      document.getElementById("uploadStatus").textContent = prog.message + " | Tempo Estimado: " + prog.eta;
    }} catch(e) {{}}
  }}

  let editingActive = false;

  async function startEditing() {{
    if(editingActive) return;
    editingActive = true;
    document.getElementById("btnStartEdit").disabled = true;
    document.getElementById("btnStopEdit").disabled = false;
    document.getElementById("editorStatus").textContent = "Processando áudios...";

    const resp = await fetch("/start_editing_continuous", {{method: "POST"}});
    const data = await resp.json();
    document.getElementById("editorStatus").textContent = data.message;

    const pollInterval = setInterval(async () => {{
      if(!editingActive) {{
        clearInterval(pollInterval);
        document.getElementById("btnStartEdit").disabled = false;
        document.getElementById("btnStopEdit").disabled = true;
        return;
      }}
      await updateEditorProgress();
      await fetchFileCounts();
    }}, 1500);
  }}

  async function stopEditing() {{
    if(!editingActive) return;
    document.getElementById("editorStatus").textContent = "Parando após o áudio atual...";
    await fetch("/stop_editing", {{method:"POST"}});
    editingActive = false;
  }}

  async function startUpload() {{
    const perfil = document.getElementById("perfilSelect").value;
    const desc = document.getElementById("descInput").value;
    const tagsRaw = document.getElementById("tagsInput").value;
    const tags = tagsRaw.split(",").map(s => s.trim()).filter(s => s.length > 0);
    const qtdVideos = parseInt(document.getElementById("numVideos").value);
    const dataStr = document.getElementById("dateUpload").value;
    const timeStr = document.getElementById("timeUpload").value;

    if(!perfil) {{
      alert("Selecione um perfil.");
      return;
    }}
    if(!dataStr) {{
      alert("Selecione uma data futura.");
      return;
    }}
    if(!timeStr) {{
      alert("Selecione hora válida.");
      return;
    }}
    if(!qtdVideos || qtdVideos < 1) {{
      alert("Número de vídeos deve ser pelo menos 1.");
      return;
    }}

    document.getElementById("uploadStatus").textContent = "Iniciando upload/agendamento...";
    document.getElementById("progressUpload").style.width = "0%";
    document.getElementById("progressUpload").textContent = "0%";

    try {{
      const resp = await fetch("/start_upload", {{
        method: "POST",
        headers: {{ "Content-Type": "application/json" }},
        body: JSON.stringify({{
          profile_nome: perfil,
          description: desc,
          tags: tags,
          qtd_videos: qtdVideos,
          data: dataStr,
          hora: timeStr
        }})
      }});
      const data = await resp.json();
      if(resp.ok) {{
        document.getElementById("uploadStatus").textContent = data.message;
      }} else {{
        document.getElementById("uploadStatus").textContent = "Erro: " + data.message;
        return;
      }}
      const pollUpload = setInterval(async () => {{
        const res = await fetch("/progress_upload");
        const p = await res.json();
        document.getElementById("progressUpload").style.width = p.percent + "%";
        document.getElementById("progressUpload").textContent = p.percent + "%";
        document.getElementById("uploadStatus").textContent = p.message + " | ETA: " + p.eta;
        await fetchFileCounts();
        if(p.percent >= 100) {{
          clearInterval(pollUpload);
        }}
      }}, 1500);
    }} catch(e) {{
      document.getElementById("uploadStatus").textContent = "Erro ao iniciar upload: " + e.message;
    }}
  }}

  window.onload = () => {{
    checkCredentialsWarning();
    fetchFileCounts();
    const today = new Date().toISOString().slice(0,10);
    document.getElementById("dateUpload").value = today;

    setInterval(fetchFileCounts, 4000);

    document.getElementById("btnStartEdit").onclick = startEditing;
    document.getElementById("btnStopEdit").onclick = stopEditing;
    document.getElementById("btnStartUpload").onclick = startUpload;

    const ctx = document.getElementById("progressChart").getContext("2d");
    window.progressChartInstance = new Chart(ctx, {{
      type: "line",
      data: {{
        labels: [],
        datasets: [{{
          label: "Progresso Edição (%)",
          data: [],
          borderColor: "#c6a961",
          backgroundColor: "rgba(198,169,97,0.3)",
          fill: true,
          tension: 0.3,
        }}]
      }},
      options: {{
        animation: false,
        scales: {{
          y: {{min: 0, max: 100, ticks: {{color: "#e6dca8"}}, grid: {{color: "#3a2f19"}}}},
          x: {{display: false, grid: {{color: "#3a2f19"}}}}
        }},
        plugins: {{
          legend: {{labels: {{color: "#c6a961"}}}}
        }}
      }}
    }});

    setInterval(() => {{
      if(window.progressChartInstance) {{
        while(editorProgressPercentHistory.length < maxDataPoints) {{
          editorProgressPercentHistory.push(0);
        }}
        const labels = Array(editorProgressPercentHistory.length).fill("");
        window.progressChartInstance.data.labels = labels;
        window.progressChartInstance.data.datasets[0].data = editorProgressPercentHistory;
        window.progressChartInstance.update();
      }}
    }}, 3500);
  }};
</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("/file_counts_custom")
def file_counts_custom():
    audios = count_audio_files()
    videos_postagem = count_video_files()
    thumbs_carl = count_thumbnail_files()
    real_number = min(videos_postagem, thumbs_carl)
    return jsonify({"audios": audios, "videos_postagem": videos_postagem, "thumbs_carl": thumbs_carl, "real_number": real_number})

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

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

@app.route("/start_editing_continuous", methods=["POST"])
def start_editing_continuous():
    global stop_requested

    def editing_worker():
        create_video_with_audio_multiple_custom(delete_audio_after=True, stop_when_done=True)
    
    if not lock_editing_process.locked():
        thread = threading.Thread(target=editing_worker, daemon=True)
        thread.start()
        progress_editor["percent"] = 0
        progress_editor["eta"] = "--:--"
        progress_editor["message"] = "Processamento contínuo iniciado."
        stop_requested = False
        return jsonify({"message": "Processamento contínuo iniciado."})
    else:
        return jsonify({"message": "Processamento já está em execução."})

@app.route("/stop_editing", methods=["POST"])
def stop_editing():
    global stop_requested
    stop_requested = True
    progress_editor["message"] = "Parada solicitada. Terminando áudio atual antes de encerrar."
    return jsonify({"message": "Parada solicitada."})

@app.route("/perfis")
def perfis_route():
    perfis_list = get_perfis()
    perfis_json = []
    for p in perfis_list:
        id_, nome, canal, credentials = p
        has_creds = bool(credentials)
        perfis_json.append({"id": id_, "nome": nome, "canal": canal, "has_credentials": has_creds})
    return jsonify({"perfis": perfis_json})

@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 processar_envio_func():
        try:
            canal = get_canal_por_perfil(profile_nome)
            if not canal:
                progress_upload["message"] = "Perfil ou canal não encontrado."
                return
            
            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"] = "Pasta de vídeos ou thumbnails está vazia."
                return
            
            real_number = min(len(videos_all), len(thumbs_all))
            if qtd_videos > real_number:
                progress_upload["message"] = f"Quantidade solicitada ({qtd_videos}) maior que possibilidade real ({real_number})."
                return
            
            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=dt_parsed, num_slots=qtd_videos)
            except Exception as e:
                progress_upload["message"] = f"Falha ao calcular horários: {str(e)}"
                return

            enviados = 0
            total = qtd_videos

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

            conn = sqlite3.connect(DB_PATH)
            c = conn.cursor()
            c.execute("SELECT credentials FROM perfis WHERE nome=?", (profile_nome,))
            row = c.fetchone()
            conn.close()

            if not row:
                progress_upload["message"] = "Credenciais não encontradas."
                return

            credentials_blob = row[0]

            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:
                    upload_video_to_youtube(
                        credentials_blob,
                        titulo_final,
                        description,
                        tags,
                        video_file,
                        thumb_file,
                        publish_now,
                        publish_at,
                        category_id="22"
                    )
                    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)."
                    esteta_seconds = ((total - enviados) * 60)
                    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

            progress_upload["percent"] = 100
            progress_upload["eta"] = "00:00"
            progress_upload["message"] = f"Upload/agendamento concluído para {total} vídeos."
        except Exception as e:
            progress_upload["message"] = f"Erro interno no upload: {str(e)}"

    progress_upload["percent"] = 0
    progress_upload["eta"] = "--:--"
    progress_upload["message"] = "Upload/agendamento iniciado."

    thread = threading.Thread(target=processar_envio_func, daemon=True)
    thread.start()

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

if __name__ == "__main__":
    init_db()
    app.run(host="0.0.0.0", port=5000, debug=True) . integre oque pedi abaixo no codigo acima . chat preciso de uma opção para criar novos perfils na area de agendador. e também quero que adicione essa função separada em uma parte da interface essa nova função . import os
import re
import threading
import time
import subprocess
import tkinter as tk
from tkinter import Frame, Button, Label, messagebox
from tkinter import scrolledtext

DOWNLOADS_FOLDER = r'C:\Users\jonathan\Downloads'
OUTPUT_CARL_JUNG = r"C:\Users\jonathan\Desktop\AUTLAND\AUDIOS\AUDIOS CARL JUNG"

titulos = []
monitorando = False

def limpar_nome(texto):
return re.sub(r'[\/*?:"<>|]', "", texto.strip())

def atualizar_titulos():
global titulos
texto = campo_titulos.get("1.0", tk.END).strip()
linhas = texto.split("\n")
titulos[:] = [limpar_nome(l) for l in linhas if l.strip()]
print("Títulos atualizados:", titulos)

def atualizar_titulos_na_interface():
campo_titulos.delete("1.0", tk.END)
for titulo in titulos:
campo_titulos.insert(tk.END, titulo + "\n")

def converter_para_mp3(caminho_video, caminho_saida):
print(f"Convertendo {caminho_video} em {caminho_saida}")
comando = [
'ffmpeg', '-y', '-i', caminho_video, '-vn',
'-acodec', 'libmp3lame', '-q:a', '3', caminho_saida
]
result = subprocess.run(comando, capture_output=True, text=True)
if result.returncode != 0:
print("Erro na conversão:", result.stderr)
raise Exception(f"Erro na conversão:\n{result.stderr}")

def juntar_mp3(arquivos_entrada, arquivo_saida):
print(f"Juntando os arquivos: {arquivos_entrada} em {arquivo_saida}")
lista_txt = os.path.join(DOWNLOADS_FOLDER, 'lista.txt')
with open(lista_txt, 'w', encoding='utf-8') as f:
for arquivo in arquivos_entrada:
caminho_formatado = arquivo.replace('\', '/')
f.write(f"file '{caminho_formatado}'\n")


comando = [
    'ffmpeg', '-y', '-f', 'concat', '-safe', '0',
    '-i', lista_txt, '-c', 'copy', arquivo_saida
]
result = subprocess.run(comando, capture_output=True, text=True)
if result.returncode != 0:
    print("Erro na junção:", result.stderr)
    raise Exception(f"Erro na junção:\n{result.stderr}")

os.remove(lista_txt)
def processar_videos(arquivos):
try:
print("Iniciando processamento dos arquivos:", arquivos)
caminhos_videos = [os.path.join(DOWNLOADS_FOLDER, a) for a in arquivos]


    mp3_arquivos = []
    for idx, caminho in enumerate(caminhos_videos):
        mp3_nome = f"temp_audio_{idx}.mp3"
        mp3_caminho = os.path.join(DOWNLOADS_FOLDER, mp3_nome)
        converter_para_mp3(caminho, mp3_caminho)
        mp3_arquivos.append(mp3_caminho)

    if titulos:
        nome_saida = limpar_nome(titulos.pop(0)) + ".mp3"
    else:
        nome_saida = "output.mp3"

    atualizar_titulos_na_interface()

    if not os.path.exists(OUTPUT_CARL_JUNG):
        os.makedirs(OUTPUT_CARL_JUNG)

    caminho_saida = os.path.join(OUTPUT_CARL_JUNG, nome_saida)

    juntar_mp3(mp3_arquivos, caminho_saida)

    # Remove arquivos temporários e vídeos originais
    for arquivo in mp3_arquivos + caminhos_videos:
        if os.path.exists(arquivo):
            os.remove(arquivo)

    print(f"Arquivo salvo em {caminho_saida}")
except Exception as e:
    messagebox.showerror("Erro", str(e))
    print("Erro:", e)
def monitorar_pasta():
global monitorando
while monitorando:
try:
arquivos_video = [
f for f in os.listdir(DOWNLOADS_FOLDER)
if f.lower().endswith(('.mp4', '.mkv', '.avi', '.mov'))
and not f.endswith('.crdownload')
]
print("Vídeos encontrados:", arquivos_video)


        if len(arquivos_video) >= 4:
            arquivos_ordenados = sorted(
                arquivos_video,
                key=lambda x: os.path.getmtime(os.path.join(DOWNLOADS_FOLDER, x))
            )
            arquivos_a_processar = arquivos_ordenados[:4]
            print("Processando vídeos:", arquivos_a_processar)
            processar_videos(arquivos_a_processar)
        else:
            print("Aguardando... Menos de 4 vídeos na pasta.")

        time.sleep(3)
    except Exception as e:
        print("Erro no monitoramento:", e)
        time.sleep(5)
def iniciar_monitoramento():
global monitorando
if not monitorando:
atualizar_titulos()
monitorando = True
threading.Thread(target=monitorar_pasta, daemon=True).start()
label_status.config(text="Status: Monitorando", fg="green")
else:
monitorando = False
label_status.config(text="Status: Parado", fg="red")

Criando a interface gráfica mínima
root = tk.Tk()
root.title("Monitor de Conversão - Carl Jung")
root.geometry("600x400")

frame = Frame(root)
frame.pack(fill="both", expand=True, padx=10, pady=10)

tk.Label(frame, text="Títulos (um por linha):").pack(anchor="w")

campo_titulos = scrolledtext.ScrolledText(frame, height=10)
campo_titulos.pack(fill="both", expand=True)

btn_iniciar = Button(frame, text="Ativar / Desativar Monitoramento", command=iniciar_monitoramento, bg="orange")
btn_iniciar.pack(pady=10)

label_status = Label(frame, text="Status: Parado", fg="red")
label_status.pack()

root.mainloop() .