510 lines
18 KiB
PHP
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>
|