36896-vm/index.php
Flatlogic Bot 48cd368984 cloud
2025-12-12 17:05:57 +00:00

510 lines
18 KiB
PHP

<!DOCTYPE html>
<html lang="pt-BR">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CloudStream - Conversor de Vídeo</title>
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
: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;
padding: 2rem;
}
.container {
max-width: 1000px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 2rem;
}
header {
text-align: center;
}
header h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary);
letter-spacing: -1px;
}
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);
}
.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; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>CloudStream</h1>
<p>Converta seus vídeos M3U8 para MP4 de forma simples e rápida.</p>
</header>
<div class="card">
<div class="form-grid">
<div class="input-group" style="grid-column: 1 / -1;">
<label for="stream-url">URL do Stream (M3U8 ou MP4)</label>
<input type="url" id="stream-url" placeholder="https://exemplo.com/stream.m3u8" value="https://live-hls-abr-cdn.livepush.io/live/bigbuckbunnyclip/index.m3u8">
</div>
<div class="input-group">
<label for="filename">Nome do Arquivo (sem extensão)</label>
<input type="text" id="filename" placeholder="meu-video-incrivel" value="bigbuckbunny-clip">
</div>
<div class="input-group">
<label for="dropbox-token">Token do Dropbox (Opcional)</label>
<input type="password" id="dropbox-token" placeholder="Cole seu token de acesso aqui">
</div>
</div>
<div style="text-align: right; margin-top: 1.5rem;">
<button id="save-btn" class="btn btn-primary">💾 Salvar na Nuvem</button>
</div>
</div>
<div class="video-section card">
<h3>Pré-visualização</h3>
<video id="preview" controls></video>
</div>
<div class="file-list card">
<h2>Meus Arquivos</h2>
<table>
<thead>
<tr>
<th>Nome</th>
<th>Status</th>
<th>Criação</th>
<th style="text-align: right;">Ações</th>
</tr>
</thead>
<tbody>
<?php
require_once 'db/config.php';
try {
$pdo = db();
$stmt = $pdo->query("SELECT id, filename, status, created_at, url, converted_path FROM streams ORDER BY created_at DESC");
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
$status = htmlspecialchars($row['status']);
$statusClass = 'status-' . strtolower($status);
echo "<tr>";
echo "<td>" . htmlspecialchars($row['filename']) . "</td>";
echo "<td><span class='status-badge " . $statusClass . "'>" . $status . "</span></td>";
echo "<td>" . htmlspecialchars($row['created_at']) . "</td>";
echo "<td class='actions' style='justify-content: flex-end;'>";
// Botão Play
$playUrl = ($status === 'completed' && !empty($row['converted_path']))
? 'videos/' . htmlspecialchars($row['converted_path'], ENT_QUOTES)
: htmlspecialchars($row['url'], ENT_QUOTES);
echo '<button class="btn btn-secondary" onclick="playVideo(\'' . $playUrl . '\')">▶️ Play</button>';
// Botão Converter foi removido pois a conversão agora é automática
// Botão Dropbox
if ($status === 'completed') {
echo '<button class="btn btn-dropbox" onclick="sendToDropbox(' . $row['id'] . ', this)" title="Enviar para Dropbox">☁️</button>';
}
// Botão Deletar
echo '<button class="btn btn-danger" onclick="deleteVideo(' . $row['id'] . ', this)" title="Deletar">🗑️</button>';
echo "</td>";
echo "</tr>";
}
if ($stmt->rowCount() === 0) {
echo "<tr><td colspan='4' style='text-align: center; padding: 2rem; color: var(--on-surface); opacity: 0.7;'>Nenhum arquivo encontrado. Adicione um stream para começar.</td></tr>";
}
} catch (PDOException $e) {
echo "<tr><td colspan='4' style='text-align:center; color: var(--error);'>Erro ao carregar arquivos: " . $e->getMessage() . "</td></tr>";
}
?>
</tbody>
</table>
</div>
</div>
<div id="toast-container"></div>
<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) {
document.getElementById('stream-url').value = url;
playUrlInPreviewer(url);
document.querySelector('.video-section').scrollIntoView({ behavior: 'smooth' });
}
document.addEventListener('DOMContentLoaded', () => {
const streamUrlInput = document.getElementById('stream-url');
if (streamUrlInput.value) {
playUrlInPreviewer(streamUrlInput.value);
}
streamUrlInput.addEventListener('input', () => {
playUrlInPreviewer(streamUrlInput.value);
});
const saveBtn = document.getElementById('save-btn');
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 e Convertendo...';
try {
const saveResponse = await fetch('api.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'save', url, filename, token }),
});
const saveResult = await saveResponse.json();
if (!saveResponse.ok) throw new Error(saveResult.error || 'Erro ao salvar o stream');
showToast(saveResult.message, 'info');
await convertVideo(saveResult.job_id, saveBtn);
} catch (err) {
showToast(err.message, 'error');
saveBtn.disabled = false;
saveBtn.textContent = '💾 Salvar na Nuvem';
}
});
});
async function convertVideo(id, button) {
button.disabled = true;
button.innerHTML = '⚙️ Convertendo...';
showToast('A conversão foi iniciada. Isso pode levar vários minutos.', 'info');
try {
const response = await fetch('api.php?action=convert_to_mp4', {
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 na conversão');
showToast(result.message, 'success');
setTimeout(() => location.reload(), 1500);
} catch (err) {
showToast(err.message, 'error');
button.disabled = false;
button.innerHTML = '💾 Salvar na Nuvem';
}
}
async function sendToDropbox(id, button) {
const token = document.getElementById('dropbox-token').value;
if (!token) {
showToast('Por favor, insira seu token de acesso do Dropbox.', 'error');
return;
}
button.disabled = true;
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);
toast.getBoundingClientRect();
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 5000);
}
</script>
</body>
</html>