This commit is contained in:
Flatlogic Bot 2026-02-27 02:45:40 +00:00
parent 4e986c09da
commit cf734ceaa6
9 changed files with 1415 additions and 779 deletions

44
api.php
View File

@ -3,12 +3,14 @@ header('Content-Type: application/json');
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/db/config.php'; require_once __DIR__ . '/db/config.php';
use Kunnu\Dropbox\Dropbox;
use Kunnu\Dropbox\DropboxApp;
use Kunnu\Dropbox\DropboxFile;
use Kunnu\Dropbox\Exceptions\DropboxClientException;
$response = []; $response = [];
$action = $_GET['action'] ?? ''; $action = $_GET['action'] ?? '';
use Dropbox\Client;
use Dropbox\Exception;
switch ($action) { switch ($action) {
case 'save': case 'save':
if ($_SERVER['REQUEST_METHOD'] === 'POST') { if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@ -64,26 +66,6 @@ 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);
@ -103,7 +85,6 @@ switch ($action) {
http_response_code(404); http_response_code(404);
$response['error'] = 'Stream não encontrado.'; $response['error'] = 'Stream não encontrado.';
} else { } else {
// Get video duration using ffprobe
$ffprobe_command = [ $ffprobe_command = [
'ffprobe', 'ffprobe',
'-v', 'error', '-v', 'error',
@ -197,16 +178,16 @@ switch ($action) {
$last_match = end($matches[1]); $last_match = end($matches[1]);
if ($last_match) { if ($last_match) {
$processed_ms = (float)$last_match / 1000000; // a unidade está em microsegundos $processed_ms = (float)$last_match / 1000000;
$duration = (float)$stream['duration']; $duration = (float)$stream['duration'];
$progress = $duration > 0 ? round(($processed_ms / $duration) * 100) : 0; $progress = $duration > 0 ? round(($processed_ms / $duration) * 100) : 0;
$progress = min(100, $progress); // Garante que o progresso não passe de 100 $progress = min(100, $progress);
$pdo->prepare("UPDATE streams SET progress = :progress WHERE id = :id")->execute([':progress' => $progress, ':id' => $id]); $pdo->prepare("UPDATE streams SET progress = :progress WHERE id = :id")->execute([':progress' => $progress, ':id' => $id]);
if ($progress == 100) { if ($progress == 100) {
$pdo->prepare("UPDATE streams SET status = 'completed', converted_path = :converted_path WHERE id = :id")->execute([':converted_path' => $output_filename, ':id' => $id]); $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 if (file_exists($progress_log_path)) unlink($progress_log_path);
} }
$response['progress'] = $progress; $response['progress'] = $progress;
@ -263,11 +244,12 @@ switch ($action) {
break; break;
} }
$app = new \Dropbox\App("", "", $token); $app = new DropboxApp("", "", $token);
$dropbox = new \Dropbox\Client($app); $dropbox = new Dropbox($app);
$dropboxFileName = '/' . basename($filePath); $dropboxFileName = '/' . basename($filePath);
$file = $dropbox->upload($filePath, $dropboxFileName, ['autorename' => true]); $dropboxFile = new DropboxFile($filePath);
$file = $dropbox->upload($dropboxFile, $dropboxFileName, ['autorename' => true]);
$response['success'] = true; $response['success'] = true;
$response['message'] = 'Arquivo enviado com sucesso para o Dropbox!'; $response['message'] = 'Arquivo enviado com sucesso para o Dropbox!';
@ -276,7 +258,7 @@ switch ($action) {
} catch (PDOException $e) { } catch (PDOException $e) {
http_response_code(500); http_response_code(500);
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage(); $response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
} catch (\Dropbox\Exception $e) { } catch (DropboxClientException $e) {
http_response_code(500); http_response_code(500);
$response['error'] = 'Erro no Dropbox: ' . $e->getMessage(); $response['error'] = 'Erro no Dropbox: ' . $e->getMessage();
} catch (Exception $e) { } catch (Exception $e) {

64
api/chat.php Normal file
View File

@ -0,0 +1,64 @@
<?php
header('Content-Type: application/json');
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
$input = json_decode(file_get_contents('php://input'), true);
$message = $input['message'] ?? '';
if (empty($message)) {
echo json_encode(['reply' => "I didn't catch that. Could you repeat?"]);
exit;
}
try {
// 1. Fetch Knowledge Base (FAQs)
$stmt = db()->query("SELECT keywords, answer FROM faqs");
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
foreach ($faqs as $faq) {
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
}
// 2. Construct Prompt for AI
$systemPrompt = "You are a helpful, friendly AI assistant for this website. " .
"Use the provided Knowledge Base to answer user questions accurately. " .
"If the answer is found in the Knowledge Base, rephrase it naturally. " .
"If the answer is NOT in the Knowledge Base, use your general knowledge to help, " .
"but politely mention that you don't have specific information about that if it seems like a site-specific question. " .
"Keep answers concise and professional.\n\n" .
$knowledgeBase;
// 3. Call AI API
$response = LocalAIApi::createResponse([
'model' => 'gpt-4o-mini',
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $message],
]
]);
if (!empty($response['success'])) {
$aiReply = LocalAIApi::extractText($response);
// 4. Save to Database
try {
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
$stmt->execute([$message, $aiReply]);
} catch (Exception $e) {
error_log("DB Save Error: " . $e->getMessage());
// Continue even if save fails, so the user still gets a reply
}
echo json_encode(['reply' => $aiReply]);
} else {
// Fallback if AI fails
error_log("AI Error: " . ($response['error'] ?? 'Unknown'));
echo json_encode(['reply' => "I'm having trouble connecting to my brain right now. Please try again later."]);
}
} catch (Exception $e) {
error_log("Chat Error: " . $e->getMessage());
echo json_encode(['reply' => "An internal error occurred."]);
}

91
api/telegram_webhook.php Normal file
View File

@ -0,0 +1,91 @@
<?php
require_once __DIR__ . '/../db/config.php';
require_once __DIR__ . '/../ai/LocalAIApi.php';
// Get Telegram Update
$content = file_get_contents("php://input");
$update = json_decode($content, true);
if (!$update || !isset($update['message'])) {
exit;
}
$message = $update['message'];
$chatId = $message['chat']['id'];
$text = $message['text'] ?? '';
if (empty($text)) {
exit;
}
// Get Telegram Token from DB
$stmt = db()->query("SELECT setting_value FROM settings WHERE setting_key = 'telegram_token'");
$token = $stmt->fetchColumn();
if (!$token) {
error_log("Telegram Error: No bot token found in settings.");
exit;
}
function sendTelegramMessage($chatId, $text, $token) {
$url = "https://api.telegram.org/bot$token/sendMessage";
$data = [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'Markdown'
];
$options = [
'http' => [
'header' => "Content-type: application/x-www-form-urlencoded\r\n",
'method' => 'POST',
'content' => http_build_query($data),
],
];
$context = stream_context_create($options);
return file_get_contents($url, false, $context);
}
// Process with AI (Similar logic to api/chat.php)
try {
// 1. Fetch Knowledge Base
$stmt = db()->query("SELECT keywords, answer FROM faqs");
$faqs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$knowledgeBase = "Here is the knowledge base for this website:\n\n";
foreach ($faqs as $faq) {
$knowledgeBase .= "Q: " . $faq['keywords'] . "\nA: " . $faq['answer'] . "\n---\n";
}
$systemPrompt = "You are a helpful AI assistant integrated with Telegram. " .
"Use the provided Knowledge Base to answer user questions. " .
"Keep answers concise for mobile reading. Use Markdown for formatting.\n\n" .
$knowledgeBase;
// 2. Call AI
$response = LocalAIApi::createResponse([
'model' => 'gpt-4o-mini',
'input' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $text],
]
]);
if (!empty($response['success'])) {
$aiReply = LocalAIApi::extractText($response);
// 3. Save History
try {
$stmt = db()->prepare("INSERT INTO messages (user_message, ai_response) VALUES (?, ?)");
$stmt->execute(["[Telegram] " . $text, $aiReply]);
} catch (Exception $e) {}
// 4. Send back to Telegram
sendTelegramMessage($chatId, $aiReply, $token);
} else {
sendTelegramMessage($chatId, "I'm sorry, I encountered an error processing your request.", $token);
}
} catch (Exception $e) {
error_log("Telegram Webhook Error: " . $e->getMessage());
}

426
assets/css/app.css Normal file
View File

@ -0,0 +1,426 @@
:root {
--background: #f8fafc;
--surface: rgba(255, 255, 255, 0.7);
--primary: #6366f1;
--primary-light: #818cf8;
--secondary: #ec4899;
--accent: #06b6d4;
--text-main: #1e293b;
--text-muted: #64748b;
--success: #10b981;
--error: #ef4444;
--warning: #f59e0b;
--sidebar-width: 280px;
--glass-border: rgba(255, 255, 255, 0.4);
--shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background-color: var(--background);
color: var(--text-main);
line-height: 1.6;
overflow-x: hidden;
min-height: 100vh;
}
/* Background Blobs */
.bg-blobs {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
.blob {
position: absolute;
filter: blur(80px);
border-radius: 50%;
opacity: 0.4;
animation: blob-move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
width: 500px;
height: 500px;
background: var(--primary-light);
top: -100px;
right: -100px;
}
.blob-2 {
width: 600px;
height: 600px;
background: var(--secondary);
bottom: -150px;
left: -150px;
animation-delay: -5s;
}
.blob-3 {
width: 400px;
height: 400px;
background: var(--accent);
top: 40%;
left: 20%;
animation-delay: -10s;
}
@keyframes blob-move {
0% { transform: translate(0, 0) scale(1); }
33% { transform: translate(100px, 50px) scale(1.1); }
66% { transform: translate(-50px, 150px) scale(0.9); }
100% { transform: translate(0, 0) scale(1); }
}
.dashboard-layout {
display: flex;
min-height: 100vh;
}
/* Sidebar Styling */
.sidebar {
width: var(--sidebar-width);
background: var(--surface);
backdrop-filter: blur(20px);
border-right: 1px solid var(--glass-border);
padding: 2.5rem 1.5rem;
display: flex;
flex-direction: column;
z-index: 10;
}
.sidebar h1 {
font-size: 1.5rem;
font-weight: 800;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 3rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.sidebar nav {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.sidebar nav a {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.875rem 1.25rem;
color: var(--text-muted);
text-decoration: none;
border-radius: 12px;
font-weight: 500;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.sidebar nav a i {
font-size: 1.25rem;
}
.sidebar nav a:hover, .sidebar nav a.active {
background: white;
color: var(--primary);
box-shadow: var(--shadow);
transform: translateX(5px);
}
.sidebar nav a.active {
background: var(--primary);
color: white;
}
/* Main Content Styling */
.main-content {
flex-grow: 1;
padding: 2.5rem;
z-index: 5;
}
.container {
max-width: 1100px;
margin: 0 auto;
}
header {
margin-bottom: 2.5rem;
}
header h2 {
font-size: 2.25rem;
font-weight: 800;
letter-spacing: -0.025em;
margin-bottom: 0.5rem;
}
header p {
color: var(--text-muted);
font-size: 1.125rem;
}
/* Cards & Grid */
.card {
background: var(--surface);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 24px;
padding: 2rem;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-bottom: 2.5rem;
}
.stat-card {
background: var(--surface);
backdrop-filter: blur(20px);
padding: 1.75rem;
border-radius: 24px;
border: 1px solid var(--glass-border);
box-shadow: var(--shadow);
transition: transform 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
}
.stat-card h3 {
font-size: 0.875rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 0.75rem;
}
.stat-card .value {
font-size: 2.5rem;
font-weight: 800;
color: var(--primary);
}
/* Form Styling */
.form-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.5rem;
}
.input-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.input-group label {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-muted);
margin-left: 0.5rem;
}
.input-group input {
background: white;
border: 1px solid var(--glass-border);
padding: 0.875rem 1.25rem;
border-radius: 16px;
font-size: 1rem;
font-family: inherit;
transition: all 0.3s ease;
}
.input-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.1);
}
/* Buttons */
.btn {
padding: 1rem 1.75rem;
font-size: 1rem;
font-weight: 600;
border-radius: 16px;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary), var(--primary-light));
color: white;
box-shadow: 0 4px 15px rgba(99, 102, 241, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
}
.btn-secondary {
background: white;
color: var(--text-main);
border: 1px solid var(--glass-border);
}
.btn-secondary:hover {
background: #f1f5f9;
}
.btn-danger {
background: #fee2e2;
color: var(--error);
}
.btn-danger:hover {
background: var(--error);
color: white;
}
/* Table Styling */
.table-container {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: separate;
border-spacing: 0 0.75rem;
}
th {
text-align: left;
padding: 1rem 1.5rem;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
}
td {
padding: 1.25rem 1.5rem;
background: white;
vertical-align: middle;
}
tr td:first-child { border-radius: 16px 0 0 16px; }
tr td:last-child { border-radius: 0 16px 16px 0; }
.status-badge {
padding: 0.375rem 1rem;
border-radius: 9999px;
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
}
.status-pending { background: #fef3c7; color: #92400e; }
.status-converting { background: #dbeafe; color: #1e40af; }
.status-completed { background: #d1fae5; color: #065f46; }
.status-failed { background: #fee2e2; color: #991b1b; }
/* Progress Bar */
.progress-bar-container {
width: 120px;
height: 8px;
background: #f1f5f9;
border-radius: 9999px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--primary);
border-radius: 9999px;
transition: width 0.5s ease;
}
/* Video Preview */
video#preview {
width: 100%;
border-radius: 20px;
aspect-ratio: 16/9;
background: #000;
box-shadow: var(--shadow);
}
/* Toast */
#toast-container {
position: fixed;
bottom: 2rem;
right: 2rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
z-index: 1000;
}
.toast {
background: white;
padding: 1rem 1.5rem;
border-radius: 16px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 600;
transform: translateX(120%);
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
border-left: 6px solid var(--primary);
}
.toast.show {
transform: translateX(0);
}
.toast-success { border-color: var(--success); }
.toast-error { border-color: var(--error); }
@media (max-width: 768px) {
.dashboard-layout {
flex-direction: column;
}
.sidebar {
width: 100%;
border-right: none;
border-bottom: 1px solid var(--glass-border);
padding: 1.5rem;
}
.sidebar h1 {
margin-bottom: 1.5rem;
}
.sidebar nav {
flex-direction: row;
overflow-x: auto;
padding-bottom: 0.5rem;
}
.main-content {
padding: 1.5rem;
}
}

302
assets/css/custom.css Normal file
View File

@ -0,0 +1,302 @@
body {
background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab);
background-size: 400% 400%;
animation: gradient 15s ease infinite;
color: #212529;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
margin: 0;
min-height: 100vh;
}
.main-wrapper {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
width: 100%;
padding: 20px;
box-sizing: border-box;
position: relative;
z-index: 1;
}
@keyframes gradient {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.chat-container {
width: 100%;
max-width: 600px;
background: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 20px;
display: flex;
flex-direction: column;
height: 85vh;
box-shadow: 0 20px 40px rgba(0,0,0,0.2);
backdrop-filter: blur(15px);
-webkit-backdrop-filter: blur(15px);
overflow: hidden;
}
.chat-header {
padding: 1.5rem;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
background: rgba(255, 255, 255, 0.5);
font-weight: 700;
font-size: 1.1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 1.25rem;
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
.message {
max-width: 85%;
padding: 0.85rem 1.1rem;
border-radius: 16px;
line-height: 1.5;
font-size: 0.95rem;
box-shadow: 0 4px 15px rgba(0,0,0,0.05);
animation: fadeIn 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px) scale(0.95); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.message.visitor {
align-self: flex-end;
background: linear-gradient(135deg, #212529 0%, #343a40 100%);
color: #fff;
border-bottom-right-radius: 4px;
}
.message.bot {
align-self: flex-start;
background: #ffffff;
color: #212529;
border-bottom-left-radius: 4px;
}
.chat-input-area {
padding: 1.25rem;
background: rgba(255, 255, 255, 0.5);
border-top: 1px solid rgba(0, 0, 0, 0.05);
}
.chat-input-area form {
display: flex;
gap: 0.75rem;
}
.chat-input-area input {
flex: 1;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
padding: 0.75rem 1rem;
outline: none;
background: rgba(255, 255, 255, 0.9);
transition: all 0.3s ease;
}
.chat-input-area input:focus {
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.2);
}
.chat-input-area button {
background: #212529;
color: #fff;
border: none;
padding: 0.75rem 1.5rem;
border-radius: 12px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
}
.chat-input-area button:hover {
background: #000;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
}
/* Background Animations */
.bg-animations {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
overflow: hidden;
pointer-events: none;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.2);
border-radius: 50%;
filter: blur(80px);
animation: move 20s infinite alternate cubic-bezier(0.45, 0, 0.55, 1);
}
.blob-1 {
top: -10%;
left: -10%;
background: rgba(238, 119, 82, 0.4);
}
.blob-2 {
bottom: -10%;
right: -10%;
background: rgba(35, 166, 213, 0.4);
animation-delay: -7s;
width: 600px;
height: 600px;
}
.blob-3 {
top: 40%;
left: 30%;
background: rgba(231, 60, 126, 0.3);
animation-delay: -14s;
width: 450px;
height: 450px;
}
@keyframes move {
0% { transform: translate(0, 0) rotate(0deg) scale(1); }
33% { transform: translate(150px, 100px) rotate(120deg) scale(1.1); }
66% { transform: translate(-50px, 200px) rotate(240deg) scale(0.9); }
100% { transform: translate(0, 0) rotate(360deg) scale(1); }
}
.admin-link {
font-size: 14px;
color: #fff;
text-decoration: none;
background: rgba(0, 0, 0, 0.2);
padding: 0.5rem 1rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.admin-link:hover {
background: rgba(0, 0, 0, 0.4);
text-decoration: none;
}
/* Admin Styles */
.admin-container {
max-width: 900px;
margin: 3rem auto;
padding: 2.5rem;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-radius: 24px;
box-shadow: 0 20px 50px rgba(0,0,0,0.15);
border: 1px solid rgba(255, 255, 255, 0.4);
position: relative;
z-index: 1;
}
.admin-container h1 {
margin-top: 0;
color: #212529;
font-weight: 800;
}
.table {
width: 100%;
border-collapse: separate;
border-spacing: 0 8px;
margin-top: 1.5rem;
}
.table th {
background: transparent;
border: none;
padding: 1rem;
color: #6c757d;
font-weight: 600;
text-transform: uppercase;
font-size: 0.75rem;
letter-spacing: 1px;
}
.table td {
background: #fff;
padding: 1rem;
border: none;
}
.table tr td:first-child { border-radius: 12px 0 0 12px; }
.table tr td:last-child { border-radius: 0 12px 12px 0; }
.form-group {
margin-bottom: 1.25rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 600;
font-size: 0.9rem;
}
.form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 12px;
background: #fff;
transition: all 0.3s ease;
box-sizing: border-box;
}
.form-control:focus {
outline: none;
border-color: #23a6d5;
box-shadow: 0 0 0 3px rgba(35, 166, 213, 0.1);
}

191
assets/js/app.js Normal file
View File

@ -0,0 +1,191 @@
let hls = null;
function playUrlInPreviewer(url) {
const previewVideo = document.getElementById('preview');
if (!url || !previewVideo) return;
if (hls) {
hls.destroy();
}
if (url.endsWith('.m3u8')) {
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(url);
hls.attachMedia(previewVideo);
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."));
}
} else {
previewVideo.src = url;
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
}
}
function playVideo(url) {
const streamUrlInput = document.getElementById('stream-url');
if(streamUrlInput) {
streamUrlInput.value = url;
}
playUrlInPreviewer(url);
const videoSection = document.querySelector('.video-section');
if(videoSection){
videoSection.scrollIntoView({ behavior: 'smooth' });
}
}
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;
const originalHtml = button.innerHTML;
button.innerHTML = '...';
showToast('Enviando 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');
showToast(result.message, 'success');
button.innerHTML = '✅';
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = originalHtml;
}
}
async function deleteVideo(id, button) {
if (!confirm('Deseja deletar este vídeo?')) return;
button.disabled = true;
showToast('Apagando...', 'info');
try {
const response = await fetch('api.php?action=delete', {
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');
showToast(result.message, 'success');
button.closest('tr').remove();
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 10);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 400);
}, 4000);
}
document.addEventListener('DOMContentLoaded', () => {
const streamUrlInput = document.getElementById('stream-url');
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;
const token = document.getElementById('dropbox-token').value;
if (!url || !filename) {
showToast('Preencha a URL e o nome do arquivo.', 'error');
return;
}
saveBtn.disabled = true;
saveBtn.innerHTML = 'Salvando...';
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, 'success');
setTimeout(() => location.href = '?page=converter', 1500);
} catch (err) {
showToast(err.message, 'error');
saveBtn.disabled = false;
saveBtn.innerHTML = '💾 Salvar e Converter';
}
});
}
updateConversionProgress();
setInterval(updateConversionProgress, 5000);
});

39
assets/js/main.js Normal file
View File

@ -0,0 +1,39 @@
document.addEventListener('DOMContentLoaded', () => {
const chatForm = document.getElementById('chat-form');
const chatInput = document.getElementById('chat-input');
const chatMessages = document.getElementById('chat-messages');
const appendMessage = (text, sender) => {
const msgDiv = document.createElement('div');
msgDiv.classList.add('message', sender);
msgDiv.textContent = text;
chatMessages.appendChild(msgDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
};
chatForm.addEventListener('submit', async (e) => {
e.preventDefault();
const message = chatInput.value.trim();
if (!message) return;
appendMessage(message, 'visitor');
chatInput.value = '';
try {
const response = await fetch('api/chat.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message })
});
const data = await response.json();
// Artificial delay for realism
setTimeout(() => {
appendMessage(data.reply, 'bot');
}, 500);
} catch (error) {
console.error('Error:', error);
appendMessage("Sorry, something went wrong. Please try again.", 'bot');
}
});
});

259
cloud.php
View File

@ -1,137 +1,192 @@
<?php
require_once 'vendor/autoload.php';
require_once 'db/config.php';
use Kunnu\Dropbox\Dropbox;
use Kunnu\Dropbox\DropboxApp;
use Kunnu\Dropbox\Models\FileMetadata;
// 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());
}
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <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>My Video Cloud</title> <title>Minha Nuvem - CloudStream</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="assets/css/app.css?v=<?php echo time(); ?>">
<style> <style>
body {
background-color: #f8f9fa;
}
.video-list-item { .video-list-item {
cursor: pointer; cursor: pointer;
transition: all 0.2s ease;
border-radius: 12px;
margin-bottom: 0.5rem;
padding: 1rem;
background: white;
border: 1px solid var(--glass-border);
list-style: none;
font-weight: 500;
display: flex;
align-items: center;
gap: 0.75rem;
} }
.video-list-item:hover { .video-list-item:hover {
background-color: #e9ecef; transform: translateX(5px);
border-color: var(--primary);
color: var(--primary);
box-shadow: var(--shadow);
}
.video-list-item.active {
background: var(--primary);
color: white;
border-color: var(--primary);
}
.cloud-grid {
display: grid;
grid-template-columns: 1.5fr 1fr;
gap: 2rem;
}
@media (max-width: 992px) {
.cloud-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>
</head> </head>
<body> <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="bg-blobs">
<div class="col-md-8"> <div class="blob blob-1"></div>
<div class="mb-3"> <div class="blob blob-2"></div>
<video id="videoPlayer" width="100%" controls autoplay> <div class="blob blob-3"></div>
<source src="" type="video/mp4"> </div>
Your browser does not support the video tag.
</video> <div class="dashboard-layout">
<aside class="sidebar">
<h1>🎬 CloudStream</h1>
<nav>
<a href="index.php?page=dashboard">📊 Painel</a>
<a href="index.php?page=converter">🔄 Conversor</a>
<a href="cloud.php" class="active">☁️ Minha Nuvem</a>
</nav>
</aside>
<main class="main-content">
<div class="container">
<header>
<h2>Minha Nuvem Dropbox</h2>
<p>Gerencie e assista seus vídeos salvos diretamente da nuvem.</p>
</header>
<div class="cloud-grid">
<div class="video-player-section">
<div class="card" style="padding: 1rem;">
<video id="videoPlayer" controls autoplay style="width: 100%; border-radius: 16px; aspect-ratio: 16/9; background: #000;">
<source src="" type="video/mp4">
Seu navegador não suporta a tag de vídeo.
</video>
<div id="videoTitle" style="margin-top: 1rem; font-weight: 700; font-size: 1.25rem; color: var(--text-main);">Selecione um vídeo</div>
</div>
</div> </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; <div class="playlist-section">
use Dropbox\App; <div class="card">
use Dropbox\Exception; <h3 style="margin-bottom: 1.5rem; font-weight: 700;">Playlist na Nuvem</h3>
<ul id="videoList" style="padding: 0;">
<?php
if ($token) {
try {
$app = new DropboxApp("", "", $token);
$dropbox = new Dropbox($app);
// Fetch the latest dropbox token from the database $listFolderContents = $dropbox->listFolder('/');
try { $items = $listFolderContents->getItems();
$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) { $foundVideos = 0;
try { foreach ($items as $item) {
$app = new App("", "", $token); if ($item instanceof FileMetadata) {
$dropbox = new Client($app); $filename = strtolower($item->getName());
$videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
$files = $dropbox->listFolder('/'); $is_video = false;
$items = $files->getItems(); foreach($videoExtensions as $ext) {
if (str_ends_with($filename, $ext)) {
if (count($items) > 0) { $is_video = true;
foreach ($items as $item) { break;
$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) { if ($is_video) {
try { try {
// Get a temporary link for the file $temporaryLink = $dropbox->getTemporaryLink($item->getPathLower());
$temporaryLink = $dropbox->getTemporaryLink($metadata->getPathLower()); $link = $temporaryLink->getLink();
$link = $temporaryLink->getLink(); echo '<li class="video-list-item" data-video-src="' . htmlspecialchars($link) . '" data-video-name="' . htmlspecialchars($item->getName()) . '">
echo '<li class="list-group-item video-list-item" data-video-src="' . htmlspecialchars($link) . '">' . htmlspecialchars($metadata->getName()) . '</li>'; <span>🎥</span> ' . htmlspecialchars($item->getName()) . '
} catch (Exception $e) { </li>';
// Could not get a temporary link for some reason $foundVideos++;
error_log("Dropbox API error getting temporary link: " . $e->getMessage()); } catch (Exception $e) {}
// 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>';
} }
} }
} }
if ($foundVideos === 0) {
echo '<li style="list-style:none; color: var(--text-muted); text-align: center; padding: 2rem;">Nenhum vídeo encontrado no Dropbox.</li>';
}
} catch (Exception $e) {
echo '<li style="list-style:none; color: var(--error); padding: 1rem; background: #fee2e2; border-radius: 12px;">Erro ao conectar com Dropbox. Verifique o token no conversor.</li>';
} }
} else { } else {
echo '<li class="list-group-item">No videos found in your Dropbox.</li>'; echo '<li style="list-style:none; color: var(--text-muted); text-align: center; padding: 2rem;">Dropbox não configurado. Adicione um token no conversor primeiro.</li>';
} }
} catch (Exception $e) { ?>
error_log("Dropbox API error: " . $e->getMessage()); </ul>
if(str_contains($e->getMessage(), 'invalid_access_token')) { </div>
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>'; </div>
} 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> </div>
</div> </main>
</div>
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const videoPlayer = document.getElementById('videoPlayer'); const videoPlayer = document.getElementById('videoPlayer');
const videoListItems = document.querySelectorAll('.video-list-item'); const videoListItems = document.querySelectorAll('.video-list-item');
const videoSource = videoPlayer.querySelector('source'); const videoSource = videoPlayer.querySelector('source');
const videoTitle = document.getElementById('videoTitle');
if (videoListItems.length > 0) { function playSelectedVideo(item) {
const firstVideoSrc = videoListItems[0].getAttribute('data-video-src'); const videoSrc = item.getAttribute('data-video-src');
videoSource.setAttribute('src', firstVideoSrc); const name = item.getAttribute('data-video-name');
videoPlayer.load();
}
videoListItems.forEach(item => { videoListItems.forEach(i => i.classList.remove('active'));
item.addEventListener('click', function() { item.classList.add('active');
const videoSrc = this.getAttribute('data-video-src');
videoSource.setAttribute('src', videoSrc); videoTitle.textContent = name;
videoPlayer.load(); videoSource.setAttribute('src', videoSrc);
videoPlayer.play(); videoPlayer.load();
}); videoPlayer.play().catch(e => console.log("Autoplay blocked"));
}
if (videoListItems.length > 0) {
playSelectedVideo(videoListItems[0]);
}
videoListItems.forEach(item => {
item.addEventListener('click', function() {
playSelectedVideo(this);
}); });
}); });
</script> });
</script>
</body> </body>
</html> </html>

768
index.php
View File

@ -1,4 +1,8 @@
<?php <?php
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$page = $_GET['page'] ?? 'dashboard'; $page = $_GET['page'] ?? 'dashboard';
function get_video_stats() { function get_video_stats() {
@ -6,25 +10,21 @@ function get_video_stats() {
try { try {
$pdo = db(); $pdo = db();
$stmt = $pdo->query("SELECT status, COUNT(*) as count FROM streams GROUP BY status"); $stmt = $pdo->query("SELECT status, COUNT(*) as count FROM streams GROUP BY status");
$stats = [ $stats = ['total' => 0, 'completed' => 0, 'converting' => 0, 'pending' => 0, 'failed' => 0];
'total' => 0,
'completed' => 0,
'converting' => 0,
'pending' => 0,
'failed' => 0,
];
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$stats[strtolower($row['status'])] = $row['count']; $s = strtolower($row['status']);
$stats['total'] += $row['count']; if (isset($stats[$s])) {
$stats[$s] = (int)$row['count'];
}
$stats['total'] += (int)$row['count'];
} }
return $stats; return $stats;
} catch (PDOException $e) { } catch (Exception $e) {
return null; return ['total' => 0, 'completed' => 0, 'converting' => 0, 'pending' => 0, 'failed' => 0];
} }
} }
$video_stats = get_video_stats(); $video_stats = get_video_stats();
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang="pt-BR"> <html lang="pt-BR">
@ -32,327 +32,24 @@ $video_stats = get_video_stats();
<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 - Painel</title> <title>CloudStream - Painel</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<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 rel="stylesheet" href="assets/css/app.css?v=<?= time() ?>">
<style>
:root {
--background: #121212;
--surface: #1E1E1E;
--primary: #BB86FC;
--primary-variant: #3700B3;
--secondary: #03DAC6;
--on-background: #FFFFFF;
--on-surface: #E0E0E0;
--success: #4CAF50;
--error: #CF6679;
--warning: #FB8C00;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
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: 1200px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 2rem;
}
header {
text-align: center;
margin-bottom: 1rem;
}
header h2 {
font-size: 2rem;
font-weight: 700;
color: var(--on-background);
}
header p {
font-size: 1.1rem;
color: var(--on-surface);
opacity: 0.8;
}
.card {
background-color: var(--surface);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.2);
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));
gap: 1.5rem;
}
.input-group {
display: flex;
flex-direction: column;
}
.input-group label {
font-weight: 500;
margin-bottom: 0.5rem;
color: var(--on-surface);
}
.input-group input {
background-color: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
color: var(--on-background);
padding: 0.8rem 1rem;
border-radius: 8px;
font-size: 1rem;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-group input:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(187, 134, 252, 0.2);
}
.btn {
padding: 0.8rem 1.5rem;
font-size: 1rem;
font-weight: 600;
border-radius: 8px;
border: none;
cursor: pointer;
transition: all 0.2s ease-in-out;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.btn-primary {
background-color: var(--primary);
color: var(--background);
}
.btn-primary:hover {
background-color: #a766f8;
}
.btn-secondary {
background-color: transparent;
color: var(--secondary);
border: 2px solid var(--secondary);
}
.btn-danger {
background-color: var(--error);
color: white;
}
.btn-danger:hover {
background-color: #b04b5c;
}
.btn-dropbox {
background-color: #0061FF;
color: white;
}
.btn-dropbox:hover {
background-color: #0052d9;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.video-section {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
#preview {
width: 100%;
border-radius: 8px;
background-color: #000;
}
.file-list h2 {
font-size: 1.8rem;
margin-bottom: 1rem;
color: var(--on-surface);
border-bottom: 2px solid var(--primary);
padding-bottom: 0.5rem;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
vertical-align: middle;
}
th {
font-weight: 600;
color: var(--on-surface);
}
.status-badge {
padding: 0.3rem 0.8rem;
border-radius: 20px;
font-size: 0.8rem;
font-weight: 500;
text-transform: capitalize;
white-space: nowrap;
}
.status-pending { background-color: var(--warning); color: #000; }
.status-converting { background-color: #2196F3; color: #fff; }
.status-completed { background-color: var(--success); color: #fff; }
.status-failed { background-color: var(--error); color: #fff; }
.actions {
display: flex;
gap: 0.5rem;
}
.toast {
position: fixed;
bottom: 2rem;
right: 2rem;
background-color: var(--surface);
color: var(--on-background);
padding: 1rem 1.5rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
border-left: 4px solid;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.3s, transform 0.3s;
z-index: 1000;
}
.toast.show {
opacity: 1;
transform: translateY(0);
}
.toast-success { border-color: var(--success); }
.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> </head>
<body> <body>
<div class="bg-blobs">
<div class="blob blob-1"></div>
<div class="blob blob-2"></div>
<div class="blob blob-3"></div>
</div>
<div class="dashboard-layout"> <div class="dashboard-layout">
<aside class="sidebar"> <aside class="sidebar">
<h1>CloudStream</h1> <h1>🎬 CloudStream</h1>
<nav> <nav>
<a href="?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">📊 Painel</a> <a href="index.php?page=dashboard" class="<?= $page === 'dashboard' ? 'active' : '' ?>">📊 Painel</a>
<a href="?page=converter" class="<?= $page === 'converter' ? 'active' : '' ?>">🔄 Conversor</a> <a href="index.php?page=converter" class="<?= $page === 'converter' ? 'active' : '' ?>">🔄 Conversor</a>
<a href="cloud.php">☁️ My Video Cloud</a> <a href="cloud.php">☁️ Minha Nuvem</a>
</nav> </nav>
</aside> </aside>
@ -360,151 +57,143 @@ $video_stats = get_video_stats();
<div class="container"> <div class="container">
<?php if ($page === 'dashboard'): ?> <?php if ($page === 'dashboard'): ?>
<header> <header>
<h2>Painel de Controle</h2> <h2>Olá! 👋</h2>
<p>Visão geral do seu sistema de vídeos.</p> <p>Aqui está o resumo da sua biblioteca de vídeos.</p>
</header> </header>
<div class="stats-grid"> <div class="stats-grid">
<div class="stat-card"> <div class="stat-card">
<h3>Total de Vídeos</h3> <h3>Total de Vídeos</h3>
<div class="value"><?= htmlspecialchars($video_stats['total'] ?? 0) ?></div> <div class="value"><?= $video_stats['total'] ?></div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<h3>Concluídos</h3> <h3>Concluídos</h3>
<div class="value" style="color: var(--success)"><?= htmlspecialchars($video_stats['completed'] ?? 0) ?></div> <div class="value" style="color:var(--success)"><?= $video_stats['completed'] ?></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>
<div class="stat-card"> <div class="stat-card">
<h3>Falhas</h3> <h3>Em Processamento</h3>
<div class="value" style="color: var(--error)"><?= htmlspecialchars($video_stats['failed'] ?? 0) ?></div> <div class="value" style="color:var(--accent)"><?= $video_stats['converting'] ?></div>
</div> </div>
</div> </div>
<div class="card file-list"> <div class="card">
<h2>Últimos Arquivos</h2> <h3>Atividade Recente</h3>
<table> <div class="table-container">
<thead> <table>
<tr> <thead>
<th>Nome</th> <tr>
<th>Status</th> <th>Nome do Arquivo</th>
<th>Criação</th> <th>Status</th>
</tr> <th>Data</th>
</thead> </tr>
<tbody> </thead>
<?php <tbody>
require_once 'db/config.php'; <?php
try { try {
$pdo = db(); require_once 'db/config.php';
$stmt = $pdo->query("SELECT id, filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5"); $pdo = db();
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $stmt = $pdo->query("SELECT filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5");
$status = htmlspecialchars($row['status']); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
$statusClass = 'status-' . strtolower($status); if (empty($rows)) {
echo "<tr><td colspan='3' style='text-align:center;padding:2rem;color:var(--text-muted)'>Nenhum vídeo encontrado.</td></tr>";
echo "<tr>"; } else {
echo "<td>" . htmlspecialchars($row['filename']) . "</td>"; foreach ($rows as $row) {
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>"; $sc = 'status-' . strtolower($row['status']);
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>"; echo "<tr>";
echo "</tr>"; echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
} echo "<td><span class='status-badge $sc'>" . htmlspecialchars($row['status']) . "</span></td>";
if ($stmt->rowCount() === 0) { echo "<td style='color:var(--text-muted)'>" . date('d/m/Y H:i', strtotime($row['created_at'])) . "</td>";
echo "<tr><td colspan='3' style='text-align: center; padding: 2rem;'>Nenhum arquivo recente.</td></tr>"; echo "</tr>";
} }
} catch (PDOException $e) { }
echo "<tr><td colspan='3' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos.</td></tr>"; } catch (Exception $e) {
} echo "<tr><td colspan='3'>Erro ao carregar dados: " . htmlspecialchars($e->getMessage()) . "</td></tr>";
?> }
</tbody> ?>
</table> </tbody>
</table>
</div>
</div> </div>
<?php elseif ($page === 'converter'): ?> <?php elseif ($page === 'converter'): ?>
<header> <header>
<h2>Conversor de Vídeo</h2> <h2>🔄 Conversor HLS</h2>
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p> <p>Adicione um link de transmissão para converter e salvar.</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;">
<label for="stream-url">URL do Stream (M3U8 ou MP4)</label> <label>URL do Stream (M3U8)</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"> <input type="url" id="stream-url" placeholder="https://exemplo.com/live/playlist.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8">
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="filename">Nome do Arquivo (sem extensão)</label> <label>Nome do Arquivo de Saída</label>
<input type="text" id="filename" placeholder="meu-video-incrivel" value="bigbuckbunny-clip"> <input type="text" id="filename" value="Video_<?= date('His') ?>">
</div> </div>
<div class="input-group"> <div class="input-group">
<label for="dropbox-token">Token do Dropbox (Opcional)</label> <label>Dropbox Token (Opcional)</label>
<input type="password" id="dropbox-token" placeholder="Cole seu token de acesso aqui"> <input type="password" id="dropbox-token" placeholder="Seu token do Dropbox">
</div> </div>
</div> </div>
<div style="text-align: right; margin-top: 1.5rem;"> <div style="margin-top: 2rem; text-align: right;">
<button id="save-btn" class="btn btn-primary">💾 Salvar e Converter</button> <button id="save-btn" class="btn btn-primary">🚀 Iniciar Conversão</button>
</div> </div>
</div> </div>
<div class="video-section card"> <div class="video-section card" id="preview-section" style="display:none;">
<h3>Pré-visualização</h3> <h3>📺 Preview</h3>
<video id="preview" controls></video> <video id="preview" controls></video>
</div> </div>
<div class="file-list card"> <div class="card">
<h2>Meus Arquivos</h2> <h3>Minha Biblioteca</h3>
<table> <div class="table-container">
<thead> <table>
<tr> <thead>
<th>Nome</th> <tr>
<th>Status</th> <th>Arquivo</th>
<th>Criação</th> <th>Status</th>
<th>Progresso</th> <th>Progresso</th>
<th style="text-align: right;">Ações</th> <th style="text-align:right;">Ações</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody id="streams-table-body">
<?php <?php
require_once 'db/config.php'; try {
try { require_once 'db/config.php';
$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, url, converted_path, progress FROM streams ORDER BY created_at DESC");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$status = htmlspecialchars($row['status']); $sc = 'status-' . strtolower($row['status']);
$statusClass = 'status-' . strtolower($status); echo "<tr data-stream-id='" . $row['id'] . "' data-status='" . htmlspecialchars($row['status']) . "'>";
echo "<td style='font-weight:600;'>" . htmlspecialchars($row['filename']) . "</td>";
echo "<td><span class='status-badge $sc'>" . htmlspecialchars($row['status']) . "</span></td>";
echo "<td>";
if ($row['status'] === 'converting') {
echo "<div class='progress-bar-container'><div class='progress-bar' style='width:" . $row['progress'] . "%'></div></div>";
}
echo "</td>";
echo "<td style='text-align:right;'>";
echo "<div style='display:flex;gap:0.5rem;justify-content:flex-end;'>";
echo "<tr data-stream-id='" . $row['id'] . "' data-status='" . $status . "'>"; $playUrl = ($row['status'] === 'completed' && !empty($row['converted_path'])) ? 'videos/'.$row['converted_path'] : $row['url'];
echo "<td>" . htmlspecialchars($row['filename']) . "</td>"; echo "<button class='btn btn-secondary' onclick=\"playVideo('" . htmlspecialchars($playUrl, ENT_QUOTES) . "')\" title='Play'>▶️</button>";
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>"; if ($row['status'] === 'completed') {
echo "<td>"; echo "<button class='btn btn-secondary' onclick='sendToDropbox(" . $row['id'] . ", this)' title='Enviar para Dropbox'>☁️</button>";
if ($status === 'converting') { }
echo '<div class="progress-bar-container"><div class="progress-bar" style="width:' . ($row['progress'] ?? 0) . '%;"></div></div>';
echo "<button class='btn btn-danger' onclick='deleteVideo(" . $row['id'] . ", this)' title='Excluir'>🗑️</button>";
echo "</div>";
echo "</td>";
echo "</tr>";
} }
echo "</td>"; } catch (Exception $e) {}
echo "<td class='actions' style='justify-content: flex-end;'>"; ?>
</tbody>
$playUrl = ($status === 'completed' && !empty($row['converted_path'])) </table>
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES) </div>
: 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> </div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -513,209 +202,6 @@ $video_stats = get_video_stats();
<div id="toast-container"></div> <div id="toast-container"></div>
<script> <script src="assets/js/app.js?v=<?= time() ?>"></script>
let hls = null;
function playUrlInPreviewer(url) {
const previewVideo = document.getElementById('preview');
if (!url || !previewVideo) return;
if (hls) {
hls.destroy();
}
if (url.endsWith('.m3u8')) {
if (Hls.isSupported()) {
hls = new Hls();
hls.loadSource(url);
hls.attachMedia(previewVideo);
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."));
}
} else {
previewVideo.src = url;
previewVideo.play().catch(e => console.log("Autoplay foi bloqueado."));
}
}
function playVideo(url) {
const streamUrlInput = document.getElementById('stream-url');
if(streamUrlInput) {
streamUrlInput.value = url;
}
playUrlInPreviewer(url);
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 && 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;
const token = document.getElementById('dropbox-token').value;
if (!url || !filename) {
showToast('Preencha a URL e o nome do arquivo.', 'error');
return;
}
saveBtn.disabled = true;
saveBtn.textContent = 'Salvando...';
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, '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');
showToast(result.message, 'success');
button.innerHTML = '✅ Enviado';
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '☁️';
}
}
async function deleteVideo(id, button) {
if (!confirm('Você tem certeza que deseja deletar este vídeo? Esta ação não pode ser desfeita.')) {
return;
}
button.disabled = true;
button.innerHTML = 'Apagando...';
showToast('Apagando vídeo...', 'info');
try {
const response = await fetch('api.php?action=delete', {
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');
button.closest('tr').remove();
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '🗑️';
}
}
function showToast(message, type = 'info') {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
toast.textContent = message;
container.appendChild(toast);
setTimeout(() => {
toast.classList.add('show');
}, 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
</script>
</body> </body>
</html> </html>