diff --git a/api.php b/api.php
index 8a2e600..9198b2b 100644
--- a/api.php
+++ b/api.php
@@ -3,12 +3,14 @@ header('Content-Type: application/json');
require_once __DIR__ . '/vendor/autoload.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 = [];
$action = $_GET['action'] ?? '';
-use Dropbox\Client;
-use Dropbox\Exception;
-
switch ($action) {
case 'save':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
@@ -64,26 +66,6 @@ switch ($action) {
}
break;
- case 'get_stream':
- try {
- $id = $_GET['id'] ?? null;
- if (empty($id)) {
- http_response_code(400);
- $response['error'] = 'O ID do stream é obrigatório.';
- } else {
- $pdo = db();
- $stmt = $pdo->prepare("SELECT id, url, filename, status, created_at, progress FROM streams WHERE id = :id");
- $stmt->execute([':id' => $id]);
- $stream = $stmt->fetch(PDO::FETCH_ASSOC);
- $response['success'] = true;
- $response['stream'] = $stream;
- }
- } catch (PDOException $e) {
- http_response_code(500);
- $response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
- }
- break;
-
case 'convert_to_mp4':
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$data = json_decode(file_get_contents('php://input'), true);
@@ -103,7 +85,6 @@ switch ($action) {
http_response_code(404);
$response['error'] = 'Stream não encontrado.';
} else {
- // Get video duration using ffprobe
$ffprobe_command = [
'ffprobe',
'-v', 'error',
@@ -197,16 +178,16 @@ switch ($action) {
$last_match = end($matches[1]);
if ($last_match) {
- $processed_ms = (float)$last_match / 1000000; // a unidade está em microsegundos
+ $processed_ms = (float)$last_match / 1000000;
$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
+ $progress = min(100, $progress);
$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
+ if (file_exists($progress_log_path)) unlink($progress_log_path);
}
$response['progress'] = $progress;
@@ -263,11 +244,12 @@ switch ($action) {
break;
}
- $app = new \Dropbox\App("", "", $token);
- $dropbox = new \Dropbox\Client($app);
+ $app = new DropboxApp("", "", $token);
+ $dropbox = new Dropbox($app);
$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['message'] = 'Arquivo enviado com sucesso para o Dropbox!';
@@ -276,7 +258,7 @@ switch ($action) {
} catch (PDOException $e) {
http_response_code(500);
$response['error'] = 'Erro no banco de dados: ' . $e->getMessage();
- } catch (\Dropbox\Exception $e) {
+ } catch (DropboxClientException $e) {
http_response_code(500);
$response['error'] = 'Erro no Dropbox: ' . $e->getMessage();
} catch (Exception $e) {
@@ -343,4 +325,4 @@ switch ($action) {
break;
}
-echo json_encode($response);
+echo json_encode($response);
\ No newline at end of file
diff --git a/api/chat.php b/api/chat.php
new file mode 100644
index 0000000..dbe026c
--- /dev/null
+++ b/api/chat.php
@@ -0,0 +1,64 @@
+ "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."]);
+}
diff --git a/api/telegram_webhook.php b/api/telegram_webhook.php
new file mode 100644
index 0000000..fa4899c
--- /dev/null
+++ b/api/telegram_webhook.php
@@ -0,0 +1,91 @@
+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());
+}
diff --git a/assets/css/app.css b/assets/css/app.css
new file mode 100644
index 0000000..7547a38
--- /dev/null
+++ b/assets/css/app.css
@@ -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;
+ }
+}
diff --git a/assets/css/custom.css b/assets/css/custom.css
new file mode 100644
index 0000000..50e0502
--- /dev/null
+++ b/assets/css/custom.css
@@ -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);
+}
\ No newline at end of file
diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 0000000..e25b36c
--- /dev/null
+++ b/assets/js/app.js
@@ -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);
+});
diff --git a/assets/js/main.js b/assets/js/main.js
new file mode 100644
index 0000000..d349598
--- /dev/null
+++ b/assets/js/main.js
@@ -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');
+ }
+ });
+});
diff --git a/cloud.php b/cloud.php
index cc5d01f..15442c9 100644
--- a/cloud.php
+++ b/cloud.php
@@ -1,137 +1,192 @@
+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());
+}
+?>
-
+
- My Video Cloud
-
+ Minha Nuvem - CloudStream
+
+
-
-
Back to Dashboard
-
My Video Cloud
-
-
-
-
-
- Your browser does not support the video tag.
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Seu navegador não suporta a tag de vídeo.
+
+
Selecione um vídeo
+
-
-
-
Playlist
-
-
+
+
Playlist na Nuvem
+
+ 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());
- }
+ $listFolderContents = $dropbox->listFolder('/');
+ $items = $listFolderContents->getItems();
- 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;
+ $foundVideos = 0;
+ foreach ($items as $item) {
+ if ($item instanceof FileMetadata) {
+ $filename = strtolower($item->getName());
+ $videoExtensions = ['.mp4', '.mov', '.avi', '.mkv', '.webm'];
+ $is_video = false;
+ foreach($videoExtensions as $ext) {
+ if (str_ends_with($filename, $ext)) {
+ $is_video = true;
+ break;
+ }
}
- }
- if ($shouldDisplay) {
- try {
- // Get a temporary link for the file
- $temporaryLink = $dropbox->getTemporaryLink($metadata->getPathLower());
- $link = $temporaryLink->getLink();
- echo '' . htmlspecialchars($metadata->getName()) . ' ';
- } 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 '' . htmlspecialchars($metadata->getName()) . ' (Error) ';
+ if ($is_video) {
+ try {
+ $temporaryLink = $dropbox->getTemporaryLink($item->getPathLower());
+ $link = $temporaryLink->getLink();
+ echo '
+ 🎥 ' . htmlspecialchars($item->getName()) . '
+ ';
+ $foundVideos++;
+ } catch (Exception $e) {}
}
}
}
+
+ if ($foundVideos === 0) {
+ echo 'Nenhum vídeo encontrado no Dropbox. ';
+ }
+ } catch (Exception $e) {
+ echo 'Erro ao conectar com Dropbox. Verifique o token no conversor. ';
}
} else {
- echo 'No videos found in your Dropbox. ';
+ echo 'Dropbox não configurado. Adicione um token no conversor primeiro. ';
}
- } catch (Exception $e) {
- error_log("Dropbox API error: " . $e->getMessage());
- if(str_contains($e->getMessage(), 'invalid_access_token')) {
- echo 'Error connecting to Dropbox: The access token is invalid. Please go to the converter page and re-enter a valid token. ';
- } else {
- echo 'Error connecting to Dropbox. ';
- }
- }
- } else {
- echo 'Dropbox is not configured. Please go to the converter page, provide a token, and upload a video first. ';
- }
- ?>
-
+ ?>
+
+
+
-
+
+
-
+ });
+
+
-
+
\ No newline at end of file
diff --git a/index.php b/index.php
index ffce61a..8a49028 100644
--- a/index.php
+++ b/index.php
@@ -1,4 +1,8 @@
query("SELECT status, COUNT(*) as count FROM streams GROUP BY status");
- $stats = [
- 'total' => 0,
- 'completed' => 0,
- 'converting' => 0,
- 'pending' => 0,
- 'failed' => 0,
- ];
+ $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'];
+ $s = strtolower($row['status']);
+ if (isset($stats[$s])) {
+ $stats[$s] = (int)$row['count'];
+ }
+ $stats['total'] += (int)$row['count'];
}
return $stats;
- } catch (PDOException $e) {
- return null;
+ } catch (Exception $e) {
+ return ['total' => 0, 'completed' => 0, 'converting' => 0, 'pending' => 0, 'failed' => 0];
}
}
$video_stats = get_video_stats();
-
?>
@@ -32,327 +32,24 @@ $video_stats = get_video_stats();
CloudStream - Painel
+
-
-
+
+
@@ -360,362 +57,151 @@ $video_stats = get_video_stats();
+
Total de Vídeos
-
= htmlspecialchars($video_stats['total'] ?? 0) ?>
+
= $video_stats['total'] ?>
Concluídos
-
= htmlspecialchars($video_stats['completed'] ?? 0) ?>
-
-
-
Em Conversão
-
= htmlspecialchars($video_stats['converting'] ?? 0) ?>
+
= $video_stats['completed'] ?>
-
Falhas
-
= htmlspecialchars($video_stats['failed'] ?? 0) ?>
+
Em Processamento
+
= $video_stats['converting'] ?>
-
-
Últimos Arquivos
-
-
-
- Nome
- Status
- Criação
-
-
-
- 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 "";
- echo "" . htmlspecialchars($row['filename']) . " ";
- echo "" . $status . " ";
- echo "" . htmlspecialchars($row['created_at']) . " ";
- echo " ";
- }
- if ($stmt->rowCount() === 0) {
- echo "Nenhum arquivo recente. ";
- }
- } catch (PDOException $e) {
- echo "Erro ao carregar arquivos. ";
- }
- ?>
-
-
+
+
Atividade Recente
+
+
+
+
+ Nome do Arquivo
+ Status
+ Data
+
+
+
+ query("SELECT filename, status, created_at FROM streams ORDER BY created_at DESC LIMIT 5");
+ $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
+ if (empty($rows)) {
+ echo "Nenhum vídeo encontrado. ";
+ } else {
+ foreach ($rows as $row) {
+ $sc = 'status-' . strtolower($row['status']);
+ echo "";
+ echo "" . htmlspecialchars($row['filename']) . " ";
+ echo "" . htmlspecialchars($row['status']) . " ";
+ echo "" . date('d/m/Y H:i', strtotime($row['created_at'])) . " ";
+ echo " ";
+ }
+ }
+ } catch (Exception $e) {
+ echo "Erro ao carregar dados: " . htmlspecialchars($e->getMessage()) . " ";
+ }
+ ?>
+
+
+
- Conversor de Vídeo
- Converta seus vídeos M3U8 para MP4 de forma simples e rápida.
+ 🔄 Conversor HLS
+ Adicione um link de transmissão para converter e salvar.
+
-
-
💾 Salvar e Converter
+
+ 🚀 Iniciar Conversão
-
-
-
Pré-visualização
+
+
+
📺 Preview
-
-
Meus Arquivos
-
-
-
- Nome
- Status
- Criação
- Progresso
- Ações
-
-
-
- query("SELECT id, filename, status, created_at, url, converted_path FROM streams ORDER BY created_at DESC");
- while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
- $status = htmlspecialchars($row['status']);
- $statusClass = 'status-' . strtolower($status);
-
- echo "";
- echo "" . htmlspecialchars($row['filename']) . " ";
- echo "" . $status . " ";
- echo "" . htmlspecialchars($row['created_at']) . " ";
- echo "";
- if ($status === 'converting') {
- echo '';
+
+
Minha Biblioteca
+
+
+
+
+ Arquivo
+ Status
+ Progresso
+ Ações
+
+
+
+ query("SELECT id, filename, status, url, converted_path, progress FROM streams ORDER BY created_at DESC");
+ while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
+ $sc = 'status-' . strtolower($row['status']);
+ echo "";
+ echo "" . htmlspecialchars($row['filename']) . " ";
+ echo "" . htmlspecialchars($row['status']) . " ";
+ echo "";
+ if ($row['status'] === 'converting') {
+ echo "";
+ }
+ echo " ";
+ echo "";
+ echo "";
+
+ $playUrl = ($row['status'] === 'completed' && !empty($row['converted_path'])) ? 'videos/'.$row['converted_path'] : $row['url'];
+ echo "▶️ ";
+
+ if ($row['status'] === 'completed') {
+ echo "☁️ ";
+ }
+
+ echo "🗑️ ";
+ echo "
";
+ echo " ";
+ echo " ";
}
- echo "";
- echo "";
-
- $playUrl = ($status === 'completed' && !empty($row['converted_path']))
- ? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
- : htmlspecialchars($row['url'], ENT_QUOTES);
- echo '▶️ Play ';
-
- if ($status === 'completed') {
- echo '☁️ ';
- }
-
- echo '🗑️ ';
-
- echo " ";
- echo "";
- }
- if ($stmt->rowCount() === 0) {
- echo "Nenhum arquivo encontrado. Adicione um stream para começar. ";
- }
- } catch (PDOException $e) {
- echo "Erro ao carregar arquivos: " . $e->getMessage() . " ";
- }
- ?>
-
-
+ } catch (Exception $e) {}
+ ?>
+
+
+
-
+
-
+