saq1b's picture
Upload index.html
946b149 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reddit Video Processor</title>
<!-- Load Tailwind CSS from CDN -->
<script src="https://cdn.tailwindcss.com"></script>
</head>
<body class="bg-gray-100">
<!-- Navbar -->
<nav class="bg-white shadow">
<div class="container mx-auto px-4 py-4 flex justify-between items-center">
<h1 class="text-xl font-bold">Reddit Video Processor</h1>
<button id="openSettings" class="bg-blue-500 text-white px-4 py-2 rounded">
Settings
</button>
</div>
</nav>
<!-- Main Content -->
<div class="container mx-auto px-4 py-6">
<!-- Available Videos Section -->
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Available Videos</h2>
<div id="videoList" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Video cards will be dynamically inserted here -->
</div>
<button
id="processBtn"
class="mt-4 bg-green-500 text-white px-6 py-3 rounded"
>
Start Processing Selected Videos
</button>
</div>
<!-- Processed Videos Section -->
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Processed Videos</h2>
<div id="resultsList" class="space-y-4">
<!-- Processed videos will be dynamically inserted here -->
</div>
</div>
<!-- Live Console Output Section -->
<div class="mb-8">
<h2 class="text-2xl font-semibold mb-4">Live Console Output</h2>
<div id="consoleOutput" class="bg-black text-green-400 p-4 text-xs h-64 overflow-y-scroll rounded">
<!-- Live logs will appear here -->
</div>
</div>
</div>
<!-- Settings Modal -->
<div
id="settingsModal"
class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 hidden"
>
<div class="bg-white p-6 rounded shadow-lg w-1/3">
<h2 class="text-xl font-bold mb-4">Settings</h2>
<div>
<label class="block mb-2">Subreddits (comma separated):</label>
<input
id="subredditsInput"
type="text"
class="w-full border border-gray-300 rounded px-3 py-2"
/>
</div>
<div class="mt-4 flex justify-end">
<button
id="saveSettings"
class="bg-blue-500 text-white px-4 py-2 rounded mr-2"
>
Save
</button>
<button
id="closeSettings"
class="bg-gray-300 text-gray-700 px-4 py-2 rounded"
>
Cancel
</button>
</div>
</div>
</div>
<!-- JavaScript -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const videoList = document.getElementById("videoList");
const processBtn = document.getElementById("processBtn");
const resultsList = document.getElementById("resultsList");
const settingsModal = document.getElementById("settingsModal");
const openSettings = document.getElementById("openSettings");
const closeSettings = document.getElementById("closeSettings");
const saveSettings = document.getElementById("saveSettings");
const subredditsInput = document.getElementById("subredditsInput");
const consoleOutput = document.getElementById("consoleOutput");
let availableVideos = [];
let selectedVideoIds = new Set();
// Fetch available videos from the backend
async function fetchVideos() {
const res = await fetch("/api/videos");
availableVideos = await res.json();
renderVideoList();
}
function renderVideoList() {
videoList.innerHTML = "";
availableVideos.forEach((video) => {
const card = document.createElement("div");
card.className = "bg-white p-4 rounded shadow flex flex-col justify-between";
// Container with enforced 9:16 aspect ratio
const videoContainer = document.createElement("div");
videoContainer.className = "relative w-full rounded overflow-hidden";
videoContainer.style.aspectRatio = "9 / 16";
// Video element for preview (with controls)
const previewVideo = document.createElement("video");
previewVideo.className = "absolute top-0 left-0 w-full h-full object-cover";
previewVideo.playsInline = true;
previewVideo.controls = true; // <-- now shows native video controls
if (video.preview_video) {
previewVideo.src = video.preview_video;
} else {
previewVideo.poster = video.thumbnail;
previewVideo.src = "";
}
videoContainer.appendChild(previewVideo);
card.appendChild(videoContainer);
// Title
const title = document.createElement("h3");
title.className = "mt-2 font-semibold";
title.textContent = video.title;
card.appendChild(title);
// Info (Subreddit and Upvotes)
const info = document.createElement("p");
info.className = "text-sm text-gray-600";
info.textContent = `Subreddit: ${video.subreddit} | Upvotes: ${video.ups}`;
card.appendChild(info);
// Display Duration if available
if (video.duration) {
const durationText = document.createElement("p");
durationText.className = "text-sm text-gray-600";
durationText.textContent = `Duration: ${video.duration} sec`;
card.appendChild(durationText);
}
// Checkbox for selection
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.className = "mt-2";
checkbox.addEventListener("change", (e) => {
if (e.target.checked) {
selectedVideoIds.add(video.id);
} else {
selectedVideoIds.delete(video.id);
}
});
card.appendChild(checkbox);
videoList.appendChild(card);
});
}
processBtn.addEventListener("click", async () => {
const selectedVideos = availableVideos.filter((video) =>
selectedVideoIds.has(video.id)
);
if (selectedVideos.length === 0) {
alert("Please select at least one video to process.");
return;
}
const res = await fetch("/api/process", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(selectedVideos),
});
const data = await res.json();
console.log(data);
// Restart polling for processing results whenever processing begins.
startResultsPolling();
});
// Function to poll for processing results
let pollingInterval;
function startResultsPolling() {
// Clear any previous interval if exists
if (pollingInterval) clearInterval(pollingInterval);
pollingInterval = setInterval(async () => {
const res = await fetch("/api/results");
const results = await res.json();
renderResults(results);
// TASK 1: Stop polling if there are no pending videos
const pendingExists = Object.values(results).some(
(r) => r.status === "pending"
);
if (!pendingExists && Object.keys(results).length > 0) {
clearInterval(pollingInterval);
}
}, 5000);
}
function renderResults(results) {
resultsList.innerHTML = "";
for (const id in results) {
const result = results[id];
const container = document.createElement("div");
container.className = "bg-white p-4 rounded shadow";
const header = document.createElement("div");
header.className = "flex justify-between items-center";
const title = document.createElement("h3");
title.className = "font-semibold text-lg";
title.textContent = result.generated_title || "Processing...";
header.appendChild(title);
// TASK 5: Add a "Copy Title" button
if (result.generated_title) {
const titleCopyBtn = document.createElement("button");
titleCopyBtn.textContent = "Copy Title";
titleCopyBtn.className = "bg-blue-500 text-white px-2 py-1 rounded ml-2";
titleCopyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(result.generated_title);
});
header.appendChild(titleCopyBtn);
}
container.appendChild(header);
if (result.status === "completed") {
// Processed video display
const videoContainer = document.createElement("div");
videoContainer.className = "max-w-md mx-auto mt-2";
const videoElem = document.createElement("video");
videoElem.src = result.video_url;
videoElem.controls = true;
videoElem.className = "w-full";
videoElem.style.maxHeight = "500px";
// Make the video element draggable.
videoElem.setAttribute("draggable", "true");
videoElem.addEventListener("dragstart", (e) => {
// Build an absolute URL for the file
const fullUrl = window.location.origin + result.video_url;
// Define a file name for the dragged file (you can customize this)
const fileName = "video_" + id + ".mp4";
// Format: MIME type:filename:download_url
const downloadURLData = "video/mp4:" + fileName + ":" + fullUrl;
e.dataTransfer.setData("DownloadURL", downloadURLData);
});
videoContainer.appendChild(videoElem);
container.appendChild(videoContainer);
// Caption with a copy button
const captionDiv = document.createElement("div");
captionDiv.className = "mt-2 flex items-center";
const captionText = document.createElement("p");
captionText.textContent = result.generated_caption;
captionText.className = "flex-1";
captionDiv.appendChild(captionText);
const copyBtn = document.createElement("button");
copyBtn.textContent = "Copy Caption";
copyBtn.className = "bg-blue-500 text-white px-2 py-1 rounded ml-2";
copyBtn.addEventListener("click", () => {
navigator.clipboard.writeText(result.generated_caption);
});
captionDiv.appendChild(copyBtn);
container.appendChild(captionDiv);
} else if (result.status === "error") {
const errorMsg = document.createElement("p");
errorMsg.textContent = "Error: " + result.error;
errorMsg.className = "text-red-500 mt-2";
container.appendChild(errorMsg);
} else {
const pendingMsg = document.createElement("p");
pendingMsg.textContent = "Processing...";
pendingMsg.className = "text-gray-600 mt-2";
container.appendChild(pendingMsg);
}
// TASK 2: Add a "Delete Video" button to allow removal of processed videos.
if (result.status === "completed") {
const deleteBtn = document.createElement("button");
deleteBtn.textContent = "Delete Video";
deleteBtn.className = "bg-red-500 text-white px-2 py-1 rounded mt-2";
deleteBtn.addEventListener("click", async () => {
if (confirm("Are you sure you want to delete this video?")) {
const res = await fetch(`/api/video/${id}`, {
method: "DELETE",
});
if (res.ok) {
container.remove();
} else {
alert("Failed to delete video.");
}
}
});
container.appendChild(deleteBtn);
}
resultsList.appendChild(container);
}
}
// Settings Modal handlers
openSettings.addEventListener("click", async () => {
const res = await fetch("/api/settings");
const settings = await res.json();
subredditsInput.value = settings.subreddits.join(", ");
settingsModal.classList.remove("hidden");
});
closeSettings.addEventListener("click", () => {
settingsModal.classList.add("hidden");
});
saveSettings.addEventListener("click", async () => {
const newSubs = subredditsInput.value
.split(",")
.map((s) => s.trim())
.filter((s) => s);
const res = await fetch("/api/settings", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ subreddits: newSubs }),
});
const data = await res.json();
console.log(data);
settingsModal.classList.add("hidden");
// Refresh available videos when settings change.
fetchVideos();
});
// Connect to the logs WebSocket for live console output
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
const logsSocket = new WebSocket(`${protocol}//${location.host}/ws/logs`);
logsSocket.onmessage = function (event) {
const newLog = document.createElement("div");
newLog.textContent = event.data;
consoleOutput.appendChild(newLog);
// Auto-scroll to the bottom
consoleOutput.scrollTop = consoleOutput.scrollHeight;
};
// Initial fetch of available videos.
fetchVideos();
});
</script>
</body>
</html>