v3
This commit is contained in:
parent
48cd368984
commit
4e986c09da
150
api.php
150
api.php
@ -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()) {
|
|
||||||
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
|
|
||||||
]);
|
|
||||||
|
|
||||||
$response['success'] = true;
|
$response['success'] = true;
|
||||||
$response['message'] = 'Vídeo convertido com sucesso!';
|
$response['message'] = 'A conversão do vídeo foi iniciada.';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} 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. Vá para https://www.dropbox.com/developers/apps e clique em "Create app".
|
|
||||||
2. Escolha "Scoped Access".
|
|
||||||
3. Selecione "Full Dropbox" ou "App folder".
|
|
||||||
4. Dê 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
137
cloud.php
Normal 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>
|
||||||
13
db/migrations/003_add_progress_columns.php
Normal file
13
db/migrations/003_add_progress_columns.php
Normal 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");
|
||||||
|
}
|
||||||
|
|
||||||
11
db/migrations/004_add_output_filename_to_streams.php
Normal file
11
db/migrations/004_add_output_filename_to_streams.php
Normal 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";
|
||||||
|
}
|
||||||
|
|
||||||
308
index.php
308
index.php
@ -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,16 +328,102 @@
|
|||||||
.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>
|
||||||
|
<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>
|
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<div class="input-group" style="grid-column: 1 / -1;">
|
<div class="input-group" style="grid-column: 1 / -1;">
|
||||||
@ -258,7 +440,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="text-align: right; margin-top: 1.5rem;">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -275,6 +457,7 @@
|
|||||||
<th>Nome</th>
|
<th>Nome</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Criação</th>
|
<th>Criação</th>
|
||||||
|
<th>Progresso</th>
|
||||||
<th style="text-align: right;">Ações</th>
|
<th style="text-align: right;">Ações</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -288,46 +471,49 @@
|
|||||||
$status = htmlspecialchars($row['status']);
|
$status = htmlspecialchars($row['status']);
|
||||||
$statusClass = 'status-' . strtolower($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>" . 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>";
|
||||||
|
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;'>";
|
echo "<td class='actions' style='justify-content: flex-end;'>";
|
||||||
|
|
||||||
// Botão Play
|
|
||||||
$playUrl = ($status === 'completed' && !empty($row['converted_path']))
|
$playUrl = ($status === 'completed' && !empty($row['converted_path']))
|
||||||
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
|
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
|
||||||
: htmlspecialchars($row['url'], 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') {
|
if ($status === 'completed') {
|
||||||
echo '<button class="btn btn-dropbox" onclick="sendToDropbox(' . $row['id'] . ', this)" title="Enviar para Dropbox">☁️</button>';
|
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 '<button class="btn btn-danger" onclick="deleteVideo(' . $row['id'] . ', this)" title="Deletar">🗑️</button>';
|
||||||
|
|
||||||
echo "</td>";
|
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='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) {
|
} 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>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
</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) {
|
||||||
@ -357,23 +543,36 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function playVideo(url) {
|
function playVideo(url) {
|
||||||
document.getElementById('stream-url').value = url;
|
const streamUrlInput = document.getElementById('stream-url');
|
||||||
|
if(streamUrlInput) {
|
||||||
|
streamUrlInput.value = url;
|
||||||
|
}
|
||||||
playUrlInPreviewer(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', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const page = '<?= $page ?>';
|
||||||
|
|
||||||
|
if (page === 'converter') {
|
||||||
const streamUrlInput = document.getElementById('stream-url');
|
const streamUrlInput = document.getElementById('stream-url');
|
||||||
|
|
||||||
if (streamUrlInput.value) {
|
if (streamUrlInput && streamUrlInput.value) {
|
||||||
playUrlInPreviewer(streamUrlInput.value);
|
playUrlInPreviewer(streamUrlInput.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(streamUrlInput) {
|
||||||
streamUrlInput.addEventListener('input', () => {
|
streamUrlInput.addEventListener('input', () => {
|
||||||
playUrlInPreviewer(streamUrlInput.value);
|
playUrlInPreviewer(streamUrlInput.value);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const saveBtn = document.getElementById('save-btn');
|
const saveBtn = document.getElementById('save-btn');
|
||||||
|
if(saveBtn) {
|
||||||
saveBtn.addEventListener('click', async () => {
|
saveBtn.addEventListener('click', async () => {
|
||||||
const url = document.getElementById('stream-url').value;
|
const url = document.getElementById('stream-url').value;
|
||||||
const filename = document.getElementById('filename').value;
|
const filename = document.getElementById('filename').value;
|
||||||
@ -385,7 +584,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
saveBtn.textContent = 'Salvando e Convertendo...';
|
saveBtn.textContent = 'Salvando...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const saveResponse = await fetch('api.php', {
|
const saveResponse = await fetch('api.php', {
|
||||||
@ -396,45 +595,57 @@
|
|||||||
const saveResult = await saveResponse.json();
|
const saveResult = await saveResponse.json();
|
||||||
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
|
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
|
||||||
|
|
||||||
showToast(saveResult.message, 'info');
|
showToast(saveResult.message, 'success');
|
||||||
await convertVideo(saveResult.job_id, saveBtn);
|
setTimeout(() => location.href = '?page=converter', 1500);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
showToast(err.message, 'error');
|
||||||
saveBtn.disabled = false;
|
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) {
|
async function updateConversionProgress() {
|
||||||
button.disabled = true;
|
const convertingRows = document.querySelectorAll('tr[data-status="converting"]');
|
||||||
button.innerHTML = '⚙️ Convertendo...';
|
if (convertingRows.length === 0) return;
|
||||||
showToast('A conversão foi iniciada. Isso pode levar vários minutos.', 'info');
|
|
||||||
|
for (const row of convertingRows) {
|
||||||
|
const streamId = row.dataset.streamId;
|
||||||
|
if (!streamId) continue;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch('api.php?action=convert_to_mp4', {
|
const response = await fetch(`api.php?action=get_conversion_progress&id=${streamId}`);
|
||||||
method: 'POST',
|
if (!response.ok) continue;
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ id }),
|
|
||||||
});
|
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!response.ok) throw new Error(result.error || 'Erro na conversão');
|
if (result.success && result.progress !== undefined) {
|
||||||
|
const progressBar = row.querySelector('.progress-bar');
|
||||||
showToast(result.message, 'success');
|
if (progressBar) {
|
||||||
setTimeout(() => location.reload(), 1500);
|
progressBar.style.width = `${result.progress}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.progress >= 100) {
|
||||||
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showToast(err.message, 'error');
|
console.error('Erro ao atualizar progresso:', err);
|
||||||
button.disabled = false;
|
}
|
||||||
button.innerHTML = '💾 Salvar na Nuvem';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendToDropbox(id, button) {
|
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) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -496,14 +707,15 @@
|
|||||||
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(() => {
|
setTimeout(() => {
|
||||||
toast.classList.remove('show');
|
toast.classList.remove('show');
|
||||||
setTimeout(() => toast.remove(), 300);
|
setTimeout(() => toast.remove(), 300);
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Loading…
x
Reference in New Issue
Block a user