This commit is contained in:
Flatlogic Bot 2026-01-14 14:14:17 +00:00
parent 48cd368984
commit 4e986c09da
5 changed files with 709 additions and 252 deletions

150
api.php
View File

@ -54,7 +54,7 @@ switch ($action) {
case 'get_streams':
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, url, filename, status, created_at FROM streams ORDER BY created_at DESC");
$stmt = $pdo->query("SELECT id, url, filename, status, created_at, progress FROM streams ORDER BY created_at DESC");
$streams = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['streams'] = $streams;
@ -64,6 +64,26 @@ switch ($action) {
}
break;
case 'get_stream':
try {
$id = $_GET['id'] ?? null;
if (empty($id)) {
http_response_code(400);
$response['error'] = 'O ID do stream é obrigatório.';
} else {
$pdo = db();
$stmt = $pdo->prepare("SELECT id, url, filename, status, created_at, progress FROM streams WHERE id = :id");
$stmt->execute([':id' => $id]);
$stream = $stmt->fetch(PDO::FETCH_ASSOC);
$response['success'] = true;
$response['stream'] = $stream;
}
} catch (PDOException $e) {
http_response_code(500);
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
}
break;
case 'convert_to_mp4':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
@ -83,17 +103,37 @@ switch ($action) {
http_response_code(404);
$response['error'] = 'Stream não encontrado.';
} else {
$pdo->prepare("UPDATE streams SET status = 'converting' WHERE id = :id")->execute([':id' => $id]);
// Get video duration using ffprobe
$ffprobe_command = [
'ffprobe',
'-v', 'error',
'-show_entries', 'format=duration',
'-of', 'default=noprint_wrappers=1:nokey=1',
$stream['url']
];
$duration_process = new \Symfony\Component\Process\Process($ffprobe_command);
$duration_process->run();
$duration = (float)$duration_process->getOutput();
$output_filename = pathinfo($stream['filename'], PATHINFO_FILENAME) . '_' . time() . '.mp4';
$output_path = __DIR__ . '/videos/' . $output_filename;
$pdo->prepare("UPDATE streams SET status = 'converting', duration = :duration, output_filename = :output_filename WHERE id = :id")->execute([
':duration' => $duration,
':output_filename' => $output_filename,
':id' => $id
]);
$progress_log_path = sys_get_temp_dir() . '/' . $output_filename . '.log';
if (!is_dir(__DIR__ . '/videos')) {
mkdir(__DIR__ . '/videos', 0775, true);
}
$command = [
'ffmpeg',
'-y',
'-v', 'quiet',
'-progress', $progress_log_path,
'-i', $stream['url'],
'-c:v', 'libx264',
'-preset', 'veryfast',
@ -105,23 +145,10 @@ switch ($action) {
$process = new \Symfony\Component\Process\Process($command);
$process->setTimeout(3600);
$process->run();
if (!$process->isSuccessful()) {
http_response_code(500);
$error_output = $process->getErrorOutput();
$pdo->prepare("UPDATE streams SET status = 'error', error_message = :error WHERE id = :id")->execute([':error' => $error_output, ':id' => $id]);
$response['error'] = 'Falha na conversão do vídeo.';
} else {
$stmt = $pdo->prepare("UPDATE streams SET status = 'completed', converted_path = :converted_path WHERE id = :id");
$stmt->execute([
':converted_path' => $output_filename,
':id' => $id
]);
$process->start();
$response['success'] = true;
$response['message'] = 'Vídeo convertido com sucesso!';
}
$response['message'] = 'A conversão do vídeo foi iniciada.';
}
} catch (PDOException $e) {
http_response_code(500);
@ -137,6 +164,74 @@ switch ($action) {
}
break;
case 'get_conversion_progress':
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$id = $_GET['id'] ?? null;
if (empty($id)) {
http_response_code(400);
$response['error'] = 'O ID do stream é obrigatório.';
} else {
try {
$pdo = db();
$stmt = $pdo->prepare("SELECT duration, converted_path, output_filename FROM streams WHERE id = :id");
$stmt->execute([':id' => $id]);
$stream = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$stream) {
http_response_code(404);
$response['error'] = 'Stream não encontrado.';
} else {
$output_filename = $stream['output_filename'];
if(empty($output_filename)) {
$response['progress'] = 0;
$response['success'] = true;
} else {
$progress_log_path = sys_get_temp_dir() . '/' . $output_filename . '.log';
if (!file_exists($progress_log_path)) {
$response['progress'] = 0;
} else {
$log_content = file_get_contents($progress_log_path);
preg_match_all("/out_time_ms=(\d+)/", $log_content, $matches);
$last_match = end($matches[1]);
if ($last_match) {
$processed_ms = (float)$last_match / 1000000; // a unidade está em microsegundos
$duration = (float)$stream['duration'];
$progress = $duration > 0 ? round(($processed_ms / $duration) * 100) : 0;
$progress = min(100, $progress); // Garante que o progresso não passe de 100
$pdo->prepare("UPDATE streams SET progress = :progress WHERE id = :id")->execute([':progress' => $progress, ':id' => $id]);
if ($progress == 100) {
$pdo->prepare("UPDATE streams SET status = 'completed', converted_path = :converted_path WHERE id = :id")->execute([':converted_path' => $output_filename, ':id' => $id]);
unlink($progress_log_path); // Limpa o arquivo de log
}
$response['progress'] = $progress;
} else {
$response['progress'] = 0;
}
}
$response['success'] = true;
}
}
} catch (PDOException $e) {
http_response_code(500);
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
} catch (\Exception $e) {
http_response_code(500);
$response['error'] = 'Erro ao obter progresso: ' . $e->getMessage();
}
}
} else {
http_response_code(405);
$response['error'] = 'Método não permitido para esta ação.';
}
break;
case 'send_to_dropbox':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
@ -168,17 +263,6 @@ switch ($action) {
break;
}
/*
COMO OBTER UM TOKEN DE ACESSO DO DROPBOX:
1. para https://www.dropbox.com/developers/apps e clique em "Create app".
2. Escolha "Scoped Access".
3. Selecione "Full Dropbox" ou "App folder".
4. um nome único à sua aplicação (ex: MeuConversorDeVideo).
5. Na aba "Permissions", marque as permissões: `files.content.write`.
6. Clique em "Submit".
7. Na aba "Settings", procure pela seção "Generated access token" e clique em "Generate".
8. Copie o token gerado e cole no campo "Token do Dropbox" na interface.
*/
$app = new \Dropbox\App("", "", $token);
$dropbox = new \Dropbox\Client($app);
@ -205,11 +289,6 @@ switch ($action) {
}
break;
default:
http_response_code(400);
$response['error'] = 'Ação não especificada ou inválida.';
break;
case 'delete':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
@ -257,6 +336,11 @@ switch ($action) {
$response['error'] = 'Método não permitido para esta ação.';
}
break;
default:
http_response_code(400);
$response['error'] = 'Ação não especificada ou inválida.';
break;
}
echo json_encode($response);

137
cloud.php Normal file
View File

@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Video Cloud</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background-color: #f8f9fa;
}
.video-list-item {
cursor: pointer;
}
.video-list-item:hover {
background-color: #e9ecef;
}
</style>
</head>
<body>
<div class="container mt-5">
<a href="/" class="btn btn-secondary mb-4">Back to Dashboard</a>
<h1 class="mb-4">My Video Cloud</h1>
<div class="row">
<div class="col-md-8">
<div class="mb-3">
<video id="videoPlayer" width="100%" controls autoplay>
<source src="" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
</div>
<div class="col-md-4">
<h4>Playlist</h4>
<ul class="list-group" id="videoList">
<?php
require_once 'vendor/autoload.php';
require_once 'db/config.php';
use Dropbox\Client;
use Dropbox\App;
use Dropbox\Exception;
// Fetch the latest dropbox token from the database
try {
$pdo = db();
$stmt = $pdo->query("SELECT dropbox_token FROM streams WHERE dropbox_token IS NOT NULL AND dropbox_token != '' ORDER BY created_at DESC LIMIT 1");
$result = $stmt->fetch();
$token = $result ? $result['dropbox_token'] : null;
} catch (PDOException $e) {
$token = null;
error_log("Database error fetching token: " . $e->getMessage());
}
if ($token) {
try {
$app = new App("", "", $token);
$dropbox = new Client($app);
$files = $dropbox->listFolder('/');
$items = $files->getItems();
if (count($items) > 0) {
foreach ($items as $item) {
$metadata = $item->getMetadata();
if ($metadata instanceof \Dropbox\Models\FileMetadata) {
// Check if it's a video file based on extension
$videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
$filename = strtolower($metadata->getName());
$shouldDisplay = false;
foreach($videoExtensions as $ext) {
if (str_ends_with($filename, $ext)) {
$shouldDisplay = true;
break;
}
}
if ($shouldDisplay) {
try {
// Get a temporary link for the file
$temporaryLink = $dropbox->getTemporaryLink($metadata->getPathLower());
$link = $temporaryLink->getLink();
echo '<li class="list-group-item video-list-item" data-video-src="' . htmlspecialchars($link) . '">' . htmlspecialchars($metadata->getName()) . '</li>';
} catch (Exception $e) {
// Could not get a temporary link for some reason
error_log("Dropbox API error getting temporary link: " . $e->getMessage());
// Optionally, display an error for this specific file
echo '<li class="list-group-item disabled" title="Could not load this video.">' . htmlspecialchars($metadata->getName()) . ' (Error)</li>';
}
}
}
}
} else {
echo '<li class="list-group-item">No videos found in your Dropbox.</li>';
}
} catch (Exception $e) {
error_log("Dropbox API error: " . $e->getMessage());
if(str_contains($e->getMessage(), 'invalid_access_token')) {
echo '<li class="list-group-item">Error connecting to Dropbox: The access token is invalid. Please go to the converter page and re-enter a valid token.</li>';
} else {
echo '<li class="list-group-item">Error connecting to Dropbox.</li>';
}
}
} else {
echo '<li class="list-group-item">Dropbox is not configured. Please go to the converter page, provide a token, and upload a video first.</li>';
}
?>
</ul>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const videoPlayer = document.getElementById('videoPlayer');
const videoListItems = document.querySelectorAll('.video-list-item');
const videoSource = videoPlayer.querySelector('source');
if (videoListItems.length > 0) {
const firstVideoSrc = videoListItems[0].getAttribute('data-video-src');
videoSource.setAttribute('src', firstVideoSrc);
videoPlayer.load();
}
videoListItems.forEach(item => {
item.addEventListener('click', function() {
const videoSrc = this.getAttribute('data-video-src');
videoSource.setAttribute('src', videoSrc);
videoPlayer.load();
videoPlayer.play();
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$db = db();
$sql = "ALTER TABLE streams ADD COLUMN `duration` DECIMAL(10, 2) DEFAULT 0.00, ADD COLUMN `progress` INT DEFAULT 0";
$db->exec($sql);
echo "Migration 003 completed: Added duration and progress columns to streams table.\n";
} catch (PDOException $e) {
die("Migration 003 failed: " . $e->getMessage() . "\n");
}

View File

@ -0,0 +1,11 @@
<?php
require_once __DIR__ . '/../../db/config.php';
try {
$pdo = db();
$pdo->exec("ALTER TABLE streams ADD COLUMN output_filename VARCHAR(255) DEFAULT NULL;");
echo "Migration 004 successful: added output_filename to streams table.\n";
} catch (PDOException $e) {
echo "Error during migration 004: " . $e->getMessage() . "\n";
}

302
index.php
View File

@ -1,10 +1,37 @@
<?php
$page = $_GET['page'] ?? 'dashboard';
function get_video_stats() {
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM streams GROUP BY status");
$stats = [
'total' => 0,
'completed' => 0,
'converting' => 0,
'pending' => 0,
'failed' => 0,
];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$stats[strtolower($row['status'])] = $row['count'];
$stats['total'] += $row['count'];
}
return $stats;
} catch (PDOException $e) {
return null;
}
}
$video_stats = get_video_stats();
?>
<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CloudStream - Conversor de Vídeo</title>
<title>CloudStream - Painel</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
@ -32,11 +59,55 @@
background-color: var(--background);
color: var(--on-background);
line-height: 1.6;
}
.dashboard-layout {
display: flex;
min-height: 100vh;
}
.sidebar {
width: 250px;
background-color: var(--surface);
padding: 2rem 1rem;
border-right: 1px solid rgba(255, 255, 255, 0.1);
display: flex;
flex-direction: column;
}
.sidebar h1 {
font-size: 1.8rem;
font-weight: 700;
color: var(--primary);
letter-spacing: -1px;
text-align: center;
margin-bottom: 2rem;
}
.sidebar nav a {
display: block;
padding: 0.8rem 1rem;
color: var(--on-surface);
text-decoration: none;
border-radius: 8px;
margin-bottom: 0.5rem;
transition: background-color 0.2s, color 0.2s;
font-weight: 500;
}
.sidebar nav a.active, .sidebar nav a:hover {
background-color: var(--primary);
color: var(--background);
}
.main-content {
flex-grow: 1;
padding: 2rem;
overflow-y: auto;
}
.container {
max-width: 1000px;
max-width: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
@ -45,13 +116,13 @@
header {
text-align: center;
margin-bottom: 1rem;
}
header h1 {
font-size: 2.5rem;
header h2 {
font-size: 2rem;
font-weight: 700;
color: var(--primary);
letter-spacing: -1px;
color: var(--on-background);
}
header p {
@ -68,6 +139,31 @@
border: 1px solid rgba(255, 255, 255, 0.1);
}
/* STATS CARDS */
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
}
.stat-card {
background: var(--surface);
padding: 1.5rem;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.stat-card h3 {
font-size: 1rem;
color: var(--on-surface);
opacity: 0.8;
margin-bottom: 0.5rem;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary);
}
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@ -232,16 +328,102 @@
.toast-error { border-color: var(--error); }
.toast-info { border-color: #2196F3; }
.progress-bar-container {
width: 100%;
background-color: rgba(255, 255, 255, 0.1);
border-radius: 4px;
height: 10px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background-color: var(--secondary);
width: 0%;
transition: width 0.3s ease-in-out;
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="dashboard-layout">
<aside class="sidebar">
<h1>CloudStream</h1>
<nav>
<a href="?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">📊 Painel</a>
<a href="?page=converter" class="<?= $page === 'converter' ? 'active' : '' ?>">🔄 Conversor</a>
<a href="cloud.php">☁️ My Video Cloud</a>
</nav>
</aside>
<main class="main-content">
<div class="container">
<?php if ($page === 'dashboard'): ?>
<header>
<h2>Painel de Controle</h2>
<p>Visão geral do seu sistema de vídeos.</p>
</header>
<div class="stats-grid">
<div class="stat-card">
<h3>Total de Vídeos</h3>
<div class="value"><?= htmlspecialchars($video_stats['total'] ?? 0) ?></div>
</div>
<div class="stat-card">
<h3>Concluídos</h3>
<div class="value" style="color: var(--success)"><?= htmlspecialchars($video_stats['completed'] ?? 0) ?></div>
</div>
<div class="stat-card">
<h3>Em Conversão</h3>
<div class="value" style="color: #2196F3"><?= htmlspecialchars($video_stats['converting'] ?? 0) ?></div>
</div>
<div class="stat-card">
<h3>Falhas</h3>
<div class="value" style="color: var(--error)"><?= htmlspecialchars($video_stats['failed'] ?? 0) ?></div>
</div>
</div>
<div class="card file-list">
<h2>Últimos Arquivos</h2>
<table>
<thead>
<tr>
<th>Nome</th>
<th>Status</th>
<th>Criação</th>
</tr>
</thead>
<tbody>
<?php
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$status = htmlspecialchars($row['status']);
$statusClass = 'status-' . strtolower($status);
echo "<tr>";
echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
echo "</tr>";
}
if ($stmt->rowCount() === 0) {
echo "<tr><td colspan='3' style='text-align: center; padding: 2rem;'>Nenhum arquivo recente.</td></tr>";
}
} catch (PDOException $e) {
echo "<tr><td colspan='3' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos.</td></tr>";
}
?>
</tbody>
</table>
</div>
<?php elseif ($page === 'converter'): ?>
<header>
<h2>Conversor de Vídeo</h2>
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p>
</header>
<div class="card">
<div class="form-grid">
<div class="input-group" style="grid-column: 1 / -1;">
@ -258,7 +440,7 @@
</div>
</div>
<div style="text-align: right; margin-top: 1.5rem;">
<button id="save-btn" class="btn btn-primary">💾 Salvar na Nuvem</button>
<button id="save-btn" class="btn btn-primary">💾 Salvar e Converter</button>
</div>
</div>
@ -275,6 +457,7 @@
<th>Nome</th>
<th>Status</th>
<th>Criação</th>
<th>Progresso</th>
<th style="text-align: right;">Ações</th>
</tr>
</thead>
@ -288,41 +471,44 @@
$status = htmlspecialchars($row['status']);
$statusClass = 'status-' . strtolower($status);
echo "<tr>";
echo "<tr data-stream-id='" . $row['id'] . "' data-status='" . $status . "'>";
echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
echo "<td>";
if ($status === 'converting') {
echo '<div class="progress-bar-container"><div class="progress-bar" style="width:' . ($row['progress'] ?? 0) . '%;"></div></div>';
}
echo "</td>";
echo "<td class='actions' style='justify-content: flex-end;'>";
// Botão Play
$playUrl = ($status === 'completed' && !empty($row['converted_path']))
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
: htmlspecialchars($row['url'], ENT_QUOTES);
echo '<button class="btn btn-secondary" onclick="playVideo(\'' . $playUrl . '\')">▶️ Play</button>';
echo '<button class="btn btn-secondary" onclick="playVideo(\'" . $playUrl . "\')">▶️ Play</button>';
// Botão Converter foi removido pois a conversão agora é automática
// Botão Dropbox
if ($status === 'completed') {
echo '<button class="btn btn-dropbox" onclick="sendToDropbox(' . $row['id'] . ', this)" title="Enviar para Dropbox">☁️</button>';
}
// Botão Deletar
echo '<button class="btn btn-danger" onclick="deleteVideo(' . $row['id'] . ', this)" title="Deletar">🗑️</button>';
echo "</td>";
echo "</tr>";
}
if ($stmt->rowCount() === 0) {
echo "<tr><td colspan='4' style='text-align: center; padding: 2rem; color: var(--on-surface); opacity: 0.7;'>Nenhum arquivo encontrado. Adicione um stream para começar.</td></tr>";
echo "<tr><td colspan='5' style='text-align: center; padding: 2rem; color: var(--on-surface); opacity: 0.7;'>Nenhum arquivo encontrado. Adicione um stream para começar.</td></tr>";
}
} catch (PDOException $e) {
echo "<tr><td colspan='4' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos: " . $e->getMessage() . "</td></tr>";
echo "<tr><td colspan='5' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos: " . $e->getMessage() . "</td></tr>";
}
?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</main>
</div>
<div id="toast-container"></div>
@ -357,23 +543,36 @@
}
function playVideo(url) {
document.getElementById('stream-url').value = url;
const streamUrlInput = document.getElementById('stream-url');
if(streamUrlInput) {
streamUrlInput.value = url;
}
playUrlInPreviewer(url);
document.querySelector('.video-section').scrollIntoView({ behavior: 'smooth' });
const videoSection = document.querySelector('.video-section');
if(videoSection){
videoSection.scrollIntoView({ behavior: 'smooth' });
}
}
document.addEventListener('DOMContentLoaded', () => {
const page = '<?= $page ?>';
if (page === 'converter') {
const streamUrlInput = document.getElementById('stream-url');
if (streamUrlInput.value) {
if (streamUrlInput && streamUrlInput.value) {
playUrlInPreviewer(streamUrlInput.value);
}
if(streamUrlInput) {
streamUrlInput.addEventListener('input', () => {
playUrlInPreviewer(streamUrlInput.value);
});
}
const saveBtn = document.getElementById('save-btn');
if(saveBtn) {
saveBtn.addEventListener('click', async () => {
const url = document.getElementById('stream-url').value;
const filename = document.getElementById('filename').value;
@ -385,7 +584,7 @@
}
saveBtn.disabled = true;
saveBtn.textContent = 'Salvando e Convertendo...';
saveBtn.textContent = 'Salvando...';
try {
const saveResponse = await fetch('api.php', {
@ -396,45 +595,57 @@
const saveResult = await saveResponse.json();
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
showToast(saveResult.message, 'info');
await convertVideo(saveResult.job_id, saveBtn);
showToast(saveResult.message, 'success');
setTimeout(() => location.href = '?page=converter', 1500);
} catch (err) {
showToast(err.message, 'error');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Salvar na Nuvem';
saveBtn.textContent = '💾 Salvar e Converter';
}
});
}
}
// Always update progress, regardless of page
updateConversionProgress();
setInterval(updateConversionProgress, 5000);
});
async function convertVideo(id, button) {
button.disabled = true;
button.innerHTML = '⚙️ Convertendo...';
showToast('A conversão foi iniciada. Isso pode levar vários minutos.', 'info');
async function updateConversionProgress() {
const convertingRows = document.querySelectorAll('tr[data-status="converting"]');
if (convertingRows.length === 0) return;
for (const row of convertingRows) {
const streamId = row.dataset.streamId;
if (!streamId) continue;
try {
const response = await fetch('api.php?action=convert_to_mp4', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
const response = await fetch(`api.php?action=get_conversion_progress&id=${streamId}`);
if (!response.ok) continue;
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erro na conversão');
showToast(result.message, 'success');
setTimeout(() => location.reload(), 1500);
if (result.success && result.progress !== undefined) {
const progressBar = row.querySelector('.progress-bar');
if (progressBar) {
progressBar.style.width = `${result.progress}%`;
}
if (result.progress >= 100) {
setTimeout(() => location.reload(), 1000);
}
}
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '💾 Salvar na Nuvem';
console.error('Erro ao atualizar progresso:', err);
}
}
}
async function sendToDropbox(id, button) {
const token = document.getElementById('dropbox-token').value;
const tokenInput = document.getElementById('dropbox-token');
const token = tokenInput ? tokenInput.value : '';
if (!token) {
showToast('Por favor, insira seu token de acesso do Dropbox.', 'error');
showToast('Por favor, insira seu token de acesso do Dropbox na página do conversor.', 'error');
return;
}
@ -496,8 +707,9 @@
toast.textContent = message;
container.appendChild(toast);
toast.getBoundingClientRect();
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');