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

152
api.php
View File

@ -54,7 +54,7 @@ switch ($action) {
case 'get_streams': case 'get_streams':
try { try {
$pdo = db(); $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); $streams = $stmt->fetchAll(PDO::FETCH_ASSOC);
$response['success'] = true; $response['success'] = true;
$response['streams'] = $streams; $response['streams'] = $streams;
@ -64,6 +64,26 @@ switch ($action) {
} }
break; 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': case 'convert_to_mp4':
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
@ -83,17 +103,37 @@ switch ($action) {
http_response_code(404); http_response_code(404);
$response['error'] = 'Stream não encontrado.'; $response['error'] = 'Stream não encontrado.';
} else { } 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_filename = pathinfo($stream['filename'], PATHINFO_FILENAME) . '_' . time() . '.mp4';
$output_path = __DIR__ . '/videos/' . $output_filename; $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')) { if (!is_dir(__DIR__ . '/videos')) {
mkdir(__DIR__ . '/videos', 0775, true); mkdir(__DIR__ . '/videos', 0775, true);
} }
$command = [ $command = [
'ffmpeg', 'ffmpeg',
'-y',
'-v', 'quiet',
'-progress', $progress_log_path,
'-i', $stream['url'], '-i', $stream['url'],
'-c:v', 'libx264', '-c:v', 'libx264',
'-preset', 'veryfast', '-preset', 'veryfast',
@ -105,23 +145,10 @@ switch ($action) {
$process = new \Symfony\Component\Process\Process($command); $process = new \Symfony\Component\Process\Process($command);
$process->setTimeout(3600); $process->setTimeout(3600);
$process->run(); $process->start();
if (!$process->isSuccessful()) { $response['success'] = true;
http_response_code(500); $response['message'] = 'A conversão do vídeo foi iniciada.';
$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
]);
$response['success'] = true;
$response['message'] = 'Vídeo convertido com sucesso!';
}
} }
} catch (PDOException $e) { } catch (PDOException $e) {
http_response_code(500); http_response_code(500);
@ -137,6 +164,74 @@ switch ($action) {
} }
break; 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': case 'send_to_dropbox':
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true); $data = json_decode(file_get_contents('php://input'), true);
@ -168,17 +263,6 @@ switch ($action) {
break; 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); $app = new \Dropbox\App("", "", $token);
$dropbox = new \Dropbox\Client($app); $dropbox = new \Dropbox\Client($app);
@ -205,11 +289,6 @@ switch ($action) {
} }
break; break;
default:
http_response_code(400);
$response['error'] = 'Ação não especificada ou inválida.';
break;
case 'delete': case 'delete':
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true); $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.'; $response['error'] = 'Método não permitido para esta ação.';
} }
break; break;
default:
http_response_code(400);
$response['error'] = 'Ação não especificada ou inválida.';
break;
} }
echo json_encode($response); 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";
}

648
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> <!DOCTYPE html>
<html lang="pt-BR"> <html lang="pt-BR">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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> <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"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style> <style>
@ -32,11 +59,55 @@
background-color: var(--background); background-color: var(--background);
color: var(--on-background); color: var(--on-background);
line-height: 1.6; 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; padding: 2rem;
overflow-y: auto;
} }
.container { .container {
max-width: 1000px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -45,13 +116,13 @@
header { header {
text-align: center; text-align: center;
margin-bottom: 1rem;
} }
header h1 { header h2 {
font-size: 2.5rem; font-size: 2rem;
font-weight: 700; font-weight: 700;
color: var(--primary); color: var(--on-background);
letter-spacing: -1px;
} }
header p { header p {
@ -68,6 +139,31 @@
border: 1px solid rgba(255, 255, 255, 0.1); 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 { .form-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
@ -232,58 +328,76 @@
.toast-error { border-color: var(--error); } .toast-error { border-color: var(--error); }
.toast-info { border-color: #2196F3; } .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> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="dashboard-layout">
<header> <aside class="sidebar">
<h1>CloudStream</h1> <h1>CloudStream</h1>
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p> <nav>
</header> <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>
<div class="card"> <main class="main-content">
<div class="form-grid"> <div class="container">
<div class="input-group" style="grid-column: 1 / -1;"> <?php if ($page === 'dashboard'): ?>
<label for="stream-url">URL do Stream (M3U8 ou MP4)</label> <header>
<input type="url" id="stream-url" placeholder="https://exemplo.com/stream.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8"> <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>
<div class="input-group">
<label for="filename">Nome do Arquivo (sem extensão)</label>
<input type="text" id="filename" placeholder="meu-video-incrivel" value="bigbuckbunny-clip">
</div>
<div class="input-group">
<label for="dropbox-token">Token do Dropbox (Opcional)</label>
<input type="password" id="dropbox-token" placeholder="Cole seu token de acesso aqui">
</div>
</div>
<div style="text-align: right; margin-top: 1.5rem;">
<button id="save-btn" class="btn btn-primary">💾 Salvar na Nuvem</button>
</div>
</div>
<div class="video-section card">
<h3>Pré-visualização</h3>
<video id="preview" controls></video>
</div>
<div class="file-list card"> <div class="card file-list">
<h2>Meus Arquivos</h2> <h2>Últimos Arquivos</h2>
<table> <table>
<thead> <thead>
<tr> <tr>
<th>Nome</th> <th>Nome</th>
<th>Status</th> <th>Status</th>
<th>Criação</th> <th>Criação</th>
<th style="text-align: right;">Ações</th> </tr>
</tr> </thead>
</thead> <tbody>
<tbody> <?php
<?php
require_once 'db/config.php'; require_once 'db/config.php';
try { try {
$pdo = db(); $pdo = db();
$stmt = $pdo->query("SELECT id, filename, status, created_at, url, converted_path FROM streams ORDER BY created_at DESC"); $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)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$status = htmlspecialchars($row['status']); $status = htmlspecialchars($row['status']);
$statusClass = 'status-' . strtolower($status); $statusClass = 'status-' . strtolower($status);
@ -292,218 +406,316 @@
echo "<td>" . htmlspecialchars($row['filename']) . "</td>"; echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>"; echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>"; echo "<td>" . htmlspecialchars($row['created_at']) . "</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>';
// 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>"; echo "</tr>";
} }
if ($stmt->rowCount() === 0) { 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='3' style='text-align: center; padding: 2rem;'>Nenhum arquivo recente.</td></tr>";
} }
} catch (PDOException $e) { } 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='3' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos.</td></tr>";
} }
?> ?>
</tbody> </tbody>
</table> </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;">
<label for="stream-url">URL do Stream (M3U8 ou MP4)</label>
<input type="url" id="stream-url" placeholder="https://exemplo.com/stream.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8">
</div>
<div class="input-group">
<label for="filename">Nome do Arquivo (sem extensão)</label>
<input type="text" id="filename" placeholder="meu-video-incrivel" value="bigbuckbunny-clip">
</div>
<div class="input-group">
<label for="dropbox-token">Token do Dropbox (Opcional)</label>
<input type="password" id="dropbox-token" placeholder="Cole seu token de acesso aqui">
</div>
</div>
<div style="text-align: right; margin-top: 1.5rem;">
<button id="save-btn" class="btn btn-primary">💾 Salvar e Converter</button>
</div>
</div>
<div class="video-section card">
<h3>Pré-visualização</h3>
<video id="preview" controls></video>
</div>
<div class="file-list card">
<h2>Meus Arquivos</h2>
<table>
<thead>
<tr>
<th>Nome</th>
<th>Status</th>
<th>Criação</th>
<th>Progresso</th>
<th style="text-align: right;">Ações</th>
</tr>
</thead>
<tbody>
<?php
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, filename, status, created_at, url, converted_path FROM streams ORDER BY created_at DESC");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$status = htmlspecialchars($row['status']);
$statusClass = 'status-' . strtolower($status);
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;'>";
$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>';
if ($status === 'completed') {
echo '<button class="btn btn-dropbox" onclick="sendToDropbox(' . $row['id'] . ', this)" title="Enviar para Dropbox">☁️</button>';
}
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='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='5' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos: " . $e->getMessage() . "</td></tr>";
}
?>
</tbody>
</table>
</div>
<?php endif; ?>
</div> </div>
</div> </main>
</div>
<div id="toast-container"></div> <div id="toast-container"></div>
<script> <script>
let hls = null; let hls = null;
function playUrlInPreviewer(url) { function playUrlInPreviewer(url) {
const previewVideo = document.getElementById('preview'); const previewVideo = document.getElementById('preview');
if (!url || !previewVideo) return; if (!url || !previewVideo) return;
if (hls) { if (hls) {
hls.destroy(); hls.destroy();
} }
if (url.endsWith('.m3u8')) { if (url.endsWith('.m3u8')) {
if (Hls.isSupported()) { if (Hls.isSupported()) {
hls = new Hls(); hls = new Hls();
hls.loadSource(url); hls.loadSource(url);
hls.attachMedia(previewVideo); hls.attachMedia(previewVideo);
hls.on(Hls.Events.MANIFEST_PARSED, () => { hls.on(Hls.Events.MANIFEST_PARSED, () => {
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
});
} else if (previewVideo.canPlayType('application/vnd.apple.mpegurl')) {
previewVideo.src = url;
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado.")); previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
} });
} else { } else if (previewVideo.canPlayType('application/vnd.apple.mpegurl')) {
previewVideo.src = url; previewVideo.src = url;
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado.")); previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
} }
} else {
previewVideo.src = url;
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
} }
}
function playVideo(url) { function playVideo(url) {
document.getElementById('stream-url').value = url; const streamUrlInput = document.getElementById('stream-url');
playUrlInPreviewer(url); if(streamUrlInput) {
document.querySelector('.video-section').scrollIntoView({ behavior: 'smooth' }); streamUrlInput.value = url;
} }
playUrlInPreviewer(url);
const videoSection = document.querySelector('.video-section');
if(videoSection){
videoSection.scrollIntoView({ behavior: 'smooth' });
}
}
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const streamUrlInput = document.getElementById('stream-url'); const page = '<?= $page ?>';
if (page === 'converter') {
const streamUrlInput = document.getElementById('stream-url');
if (streamUrlInput.value) { if (streamUrlInput && streamUrlInput.value) {
playUrlInPreviewer(streamUrlInput.value); playUrlInPreviewer(streamUrlInput.value);
} }
streamUrlInput.addEventListener('input', () => { if(streamUrlInput) {
playUrlInPreviewer(streamUrlInput.value); streamUrlInput.addEventListener('input', () => {
}); playUrlInPreviewer(streamUrlInput.value);
});
}
const saveBtn = document.getElementById('save-btn'); const saveBtn = document.getElementById('save-btn');
saveBtn.addEventListener('click', async () => { if(saveBtn) {
const url = document.getElementById('stream-url').value; saveBtn.addEventListener('click', async () => {
const filename = document.getElementById('filename').value; const url = document.getElementById('stream-url').value;
const token = document.getElementById('dropbox-token').value; const filename = document.getElementById('filename').value;
const token = document.getElementById('dropbox-token').value;
if (!url || !filename) { if (!url || !filename) {
showToast('Preencha a URL e o nome do arquivo.', 'error'); showToast('Preencha a URL e o nome do arquivo.', 'error');
return; return;
} }
saveBtn.disabled = true;
saveBtn.textContent = 'Salvando e Convertendo...';
try {
const saveResponse = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save', url, filename, token }),
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
showToast(saveResult.message, 'info'); saveBtn.disabled = true;
await convertVideo(saveResult.job_id, saveBtn); saveBtn.textContent = 'Salvando...';
} catch (err) { try {
showToast(err.message, 'error'); const saveResponse = await fetch('api.php', {
saveBtn.disabled = false; method: 'POST',
saveBtn.textContent = '💾 Salvar na Nuvem'; headers: { 'Content-Type': 'application/json' },
} body: JSON.stringify({ action: 'save', url, filename, token }),
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
showToast(saveResult.message, 'success');
setTimeout(() => location.href = '?page=converter', 1500);
} catch (err) {
showToast(err.message, 'error');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Salvar e Converter';
}
});
}
}
// Always update progress, regardless of page
updateConversionProgress();
setInterval(updateConversionProgress, 5000);
});
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=get_conversion_progress&id=${streamId}`);
if (!response.ok) continue;
const result = await response.json();
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) {
console.error('Erro ao atualizar progresso:', err);
}
}
}
async function sendToDropbox(id, button) {
const tokenInput = document.getElementById('dropbox-token');
const token = tokenInput ? tokenInput.value : '';
if (!token) {
showToast('Por favor, insira seu token de acesso do Dropbox na página do conversor.', 'error');
return;
}
button.disabled = true;
button.innerHTML = 'Enviando...';
showToast('Enviando arquivo para o Dropbox...', 'info');
try {
const response = await fetch('api.php?action=send_to_dropbox', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, token }),
}); });
}); const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erro ao enviar para o Dropbox');
async function convertVideo(id, button) { showToast(result.message, 'success');
button.disabled = true; button.innerHTML = '✅ Enviado';
button.innerHTML = '⚙️ Convertendo...';
showToast('A conversão foi iniciada. Isso pode levar vários minutos.', 'info');
try { } catch (err) {
const response = await fetch('api.php?action=convert_to_mp4', { showToast(err.message, 'error');
method: 'POST', button.disabled = false;
headers: { 'Content-Type': 'application/json' }, button.innerHTML = '☁️';
body: JSON.stringify({ id }),
});
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);
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '💾 Salvar na Nuvem';
}
} }
}
async function sendToDropbox(id, button) {
const token = document.getElementById('dropbox-token').value;
if (!token) {
showToast('Por favor, insira seu token de acesso do Dropbox.', 'error');
return;
}
button.disabled = true; async function deleteVideo(id, button) {
button.innerHTML = 'Enviando...'; if (!confirm('Você tem certeza que deseja deletar este vídeo? Esta ação não pode ser desfeita.')) {
showToast('Enviando arquivo para o Dropbox...', 'info'); return;
try {
const response = await fetch('api.php?action=send_to_dropbox', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, token }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erro ao enviar para o Dropbox');
showToast(result.message, 'success');
button.innerHTML = '✅ Enviado';
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '☁️';
}
} }
async function deleteVideo(id, button) { button.disabled = true;
if (!confirm('Você tem certeza que deseja deletar este vídeo? Esta ação não pode ser desfeita.')) { button.innerHTML = 'Apagando...';
return; showToast('Apagando vídeo...', 'info');
}
button.disabled = true; try {
button.innerHTML = 'Apagando...'; const response = await fetch('api.php?action=delete', {
showToast('Apagando vídeo...', 'info'); method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erro ao apagar o vídeo');
try { showToast(result.message, 'success');
const response = await fetch('api.php?action=delete', { button.closest('tr').remove();
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id }),
});
const result = await response.json();
if (!response.ok) throw new Error(result.error || 'Erro ao apagar o vídeo');
showToast(result.message, 'success'); } catch (err) {
button.closest('tr').remove(); showToast(err.message, 'error');
button.disabled = false;
} catch (err) { button.innerHTML = '🗑️';
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '🗑️';
}
} }
}
function showToast(message, type = 'info') { function showToast(message, type = 'info') {
const container = document.getElementById('toast-container'); const container = document.getElementById('toast-container');
const toast = document.createElement('div'); const toast = document.createElement('div');
toast.className = `toast toast-${type}`; toast.className = `toast toast-${type}`;
toast.textContent = message; toast.textContent = message;
container.appendChild(toast); container.appendChild(toast);
toast.getBoundingClientRect(); setTimeout(() => {
toast.classList.add('show'); toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show'); setTimeout(() => {
setTimeout(() => toast.remove(), 300); toast.classList.remove('show');
}, 5000); setTimeout(() => toast.remove(), 300);
} }, 5000);
</script> }
</script>
</body> </body>
</html> </html>