fb-dl / templates /index.html
devusman's picture
update
a2e646a
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Facebook Video & Audio Downloader</title>
<style>
:root {
--primary-color: #1877f2;
--primary-hover: #166fe5;
--background-color: #f0f2f5;
--container-bg: #ffffff;
--text-color: #1c1e21;
--subtle-text: #606770;
--border-color: #dddfe2;
--error-bg: #fff0f0;
--error-text: #d8000c;
--progress-bg: #e4e6ea;
--progress-fill: var(--primary-color);
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
Helvetica, Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
margin: 0;
padding: 20px;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
}
.container {
background: var(--container-bg);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 600px;
transition: all 0.3s ease-in-out;
}
h1 {
text-align: center;
color: var(--primary-color);
margin-bottom: 1.5rem;
font-size: 1.8rem;
}
.tabs {
display: flex;
border-bottom: 1px solid var(--border-color);
margin-bottom: 1.5rem;
}
.tab-button {
background: none;
color: var(--subtle-text);
padding: 1rem;
border: none;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: color 0.2s, border-bottom 0.2s;
border-bottom: 3px solid transparent;
flex-grow: 1;
}
.tab-button.active {
color: var(--primary-color);
border-bottom-color: var(--primary-color);
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
form {
display: flex;
flex-direction: column;
}
input[type="text"],
select {
padding: 1rem;
margin-bottom: 1rem;
border: 1px solid var(--border-color);
border-radius: 6px;
font-size: 1rem;
width: 100%;
box-sizing: border-box;
}
button {
background-color: var(--primary-color);
color: white;
padding: 1rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
transition: background-color 0.3s;
}
button:hover:not(:disabled) {
background-color: var(--primary-hover);
}
button:disabled {
background-color: #a0bdf5;
cursor: not-allowed;
}
#status-container {
margin-top: 1.5rem;
text-align: center;
font-weight: 500;
}
#status.error {
color: var(--error-text);
background-color: var(--error-bg);
padding: 0.8rem;
border-radius: 6px;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
width: 36px;
height: 36px;
border-radius: 50%;
border-left-color: var(--primary-color);
animation: spin 1s linear infinite;
display: none;
margin: 20px auto;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.note {
font-size: 0.9rem;
color: var(--subtle-text);
margin-top: 1rem;
background-color: #f7f8fa;
padding: 0.8rem;
border-radius: 6px;
text-align: center;
}
.important-note {
font-size: 0.9rem;
color: #946c00;
margin-bottom: 1.5rem;
background-color: #fffbe6;
padding: 0.8rem;
border-radius: 6px;
text-align: center;
}
#progress-container {
display: none;
margin-top: 1rem;
text-align: center;
}
#progress-bar {
width: 100%;
height: 20px;
background-color: var(--progress-bg);
border-radius: 10px;
overflow: hidden;
display: block;
margin-bottom: 0.5rem;
}
#progress-bar::-webkit-progress-bar {
background-color: var(--progress-bg);
border-radius: 10px;
}
#progress-bar::-webkit-progress-value {
background-color: var(--progress-fill);
border-radius: 10px;
}
#progress-bar::-moz-progress-bar {
background-color: var(--progress-fill);
border-radius: 10px;
}
#progress-text {
font-weight: 600;
color: var(--subtle-text);
}
#control-buttons {
display: none;
margin-top: 1rem;
}
#pause-btn,
#cancel-btn {
margin: 0 0.5rem;
padding: 0.5rem 1rem;
font-size: 1rem;
}
#pause-btn.paused {
background-color: #28a745;
}
#pause-btn.paused:hover {
background-color: #218838;
}
#stories-results {
display: none;
margin-top: 1.5rem;
}
.story {
margin-bottom: 1.5rem;
border: 1px solid var(--border-color);
padding: 1rem;
border-radius: 8px;
background: #fafbfc;
}
.story h3 {
margin: 0 0 0.5rem 0;
color: var(--primary-color);
font-size: 1.1rem;
}
.preview-container {
text-align: center;
margin-bottom: 1rem;
}
.preview-container video,
.preview-container img,
.preview-container audio {
width: 150px;
height: 150px;
object-fit: cover;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: block;
margin: 0 auto;
}
.preview-container audio {
height: auto;
width: 150px;
}
.story ul {
list-style: none;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.story li {
flex: 1;
min-width: 120px;
}
.story a {
display: block;
padding: 0.5rem;
background: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 4px;
text-align: center;
font-size: 0.9rem;
transition: background-color 0.2s;
}
.story a:hover {
background-color: var(--primary-hover);
}
#back-to-form {
background-color: var(--subtle-text);
margin-top: 1rem;
}
#back-to-form:hover {
background-color: #525b69;
}
#stories-loader {
display: none;
text-align: center;
margin: 1rem 0;
}
</style>
</head>
<body>
<div class="container">
<h1>Facebook Video & Audio Downloader</h1>
<div class="tabs">
<button class="tab-button active" onclick="openTab(event, 'video')">
Video
</button>
<button class="tab-button" onclick="openTab(event, 'audio')">
Audio
</button>
<button class="tab-button" onclick="openTab(event, 'stories')">
Stories
</button>
</div>
<div id="video" class="tab-content active">
<form id="videoForm">
<input
type="text"
name="url"
placeholder="Enter Facebook Video URL"
required
/>
<select name="format">
<option value="mp4">MP4</option>
<option value="webm">WebM</option>
</select>
<button type="submit">Download Video</button>
</form>
<p class="note">
Download Facebook videos in MP4 or WebM format. The download will
appear in your browser's download manager.
</p>
</div>
<div id="audio" class="tab-content">
<form id="audioForm">
<input
type="text"
name="url"
placeholder="Enter Facebook Video URL"
required
/>
<select name="format">
<option value="mp3">MP3</option>
<option value="m4a">M4A</option>
<option value="wav">WAV</option>
</select>
<button type="submit">Download Audio</button>
</form>
<p class="note">
Extract audio from Facebook videos in various formats. The download
will appear in your browser's download manager.
</p>
</div>
<div id="stories" class="tab-content">
<form id="storiesForm">
<input
type="text"
name="url"
placeholder="Enter Facebook Stories URL"
required
/>
<button type="submit">Fetch Stories</button>
</form>
<div id="stories-loader" class="spinner"></div>
<p class="note">
Fetch and download Facebook stories. Previews and multiple format
options will be displayed.
</p>
<div id="stories-results">
<h2 style="text-align: center; margin-bottom: 1rem">Stories</h2>
<div id="stories-container"></div>
<button id="back-to-form" onclick="backToStoriesForm()">
Back to Form
</button>
</div>
</div>
<div id="status-container">
<div id="spinner" class="spinner"></div>
<div id="progress-container">
<progress id="progress-bar" value="0" max="100"></progress>
<div id="progress-text">0%</div>
</div>
<div id="control-buttons">
<button id="pause-btn">Pause</button>
<button id="cancel-btn" style="background-color: #dc3545">
Cancel
</button>
</div>
<div id="status"></div>
</div>
</div>
<script>
let isDownloading = false;
let paused = false;
let currentReader = null;
let currentChunks = [];
let currentReceived = 0;
let currentTotal = 0;
let currentResponse = null;
const proxy = "https://corsproxy.io/?";
const targetUrl = "https://getvidfb.com/";
function openTab(evt, tabName) {
document
.querySelectorAll(".tab-content")
.forEach((tc) => tc.classList.remove("active"));
document
.querySelectorAll(".tab-button")
.forEach((tb) => tb.classList.remove("active"));
document.getElementById(tabName).classList.add("active");
evt.currentTarget.classList.add("active");
if (tabName === "stories") {
document.getElementById("stories-results").style.display = "none";
}
}
async function handleSubmit(e) {
e.preventDefault();
if (isDownloading) {
document.getElementById("status").textContent =
"A download is already in progress. Please wait or pause/cancel the current one.";
document.getElementById("status").classList.add("error");
return;
}
const form = e.target;
const submitButton = form.querySelector('button[type="submit"]');
const status = document.getElementById("status");
const spinner = document.getElementById("spinner");
const progressContainer = document.getElementById("progress-container");
const progressBar = document.getElementById("progress-bar");
const progressText = document.getElementById("progress-text");
const controlButtons = document.getElementById("control-buttons");
// Disable all download buttons
document
.querySelectorAll('button[type="submit"]')
.forEach((btn) => (btn.disabled = true));
isDownloading = true;
paused = false;
spinner.style.display = "block";
submitButton.disabled = true;
status.textContent =
"Initializing... Please wait, this may take a moment.";
status.classList.remove("error");
progressContainer.style.display = "none";
progressBar.value = 0;
progressText.textContent = "0%";
controlButtons.style.display = "none";
try {
const formData = new FormData(form);
const response = await fetch("/download", {
method: "POST",
body: formData,
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || "An unknown server error occurred.");
}
status.textContent = "Streaming download...";
spinner.style.display = "none";
progressContainer.style.display = "block";
controlButtons.style.display = "block";
currentTotal = parseInt(
response.headers.get("Content-Length") || "0"
);
currentReceived = 0;
currentChunks = [];
currentReader = response.body.getReader();
currentResponse = response;
// Set up pause button
const pauseBtn = document.getElementById("pause-btn");
pauseBtn.onclick = togglePause;
pauseBtn.textContent = "Pause";
pauseBtn.classList.remove("paused");
// Set up cancel button
const cancelBtn = document.getElementById("cancel-btn");
cancelBtn.onclick = cancelDownload;
readChunk();
} catch (error) {
status.textContent = `Error: ${error.message}`;
status.classList.add("error");
resetDownloadState();
}
}
async function handleStoriesSubmit(e) {
e.preventDefault();
const form = e.target;
const urlInput = form.querySelector('input[name="url"]');
const fbUrl = urlInput.value;
const loader = document.getElementById("stories-loader");
const results = document.getElementById("stories-results");
const container = document.getElementById("stories-container");
const status = document.getElementById("status");
if (!fbUrl) return;
form.querySelector('button[type="submit"]').disabled = true;
loader.style.display = "block";
status.textContent = "Fetching stories...";
status.classList.remove("error");
results.style.display = "none";
container.innerHTML = "";
try {
const proxiedUrl = proxy + encodeURIComponent(targetUrl);
const formData = new URLSearchParams();
formData.append("url", fbUrl);
formData.append("lang", "en");
formData.append("type", "redirect");
const response = await fetch(proxiedUrl, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: formData,
});
if (!response.ok) throw new Error("Failed to fetch stories");
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
const storyBlocks = doc.querySelectorAll(".snaptikvid");
if (storyBlocks.length === 0) {
// Fallback for single content
const links = doc.querySelectorAll("#snaptik-video a.abutton");
const storyDiv = document.createElement("div");
storyDiv.className = "story";
storyDiv.innerHTML = "<h3>Story #0</h3>";
const previewContainer = document.createElement("div");
previewContainer.className = "preview-container";
let previewAdded = false;
const ul = document.createElement("ul");
links.forEach((link) => {
const title =
link.querySelector("span")?.textContent.trim() || "Download";
const href = link.getAttribute("href");
const li = document.createElement("li");
let preview;
if (!previewAdded && title.includes("Video/Mp4")) {
preview = document.createElement("video");
preview.controls = true;
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
} else if (!previewAdded && title.includes("Photo/Jpg")) {
preview = document.createElement("img");
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
} else if (!previewAdded && title.includes("Audio/Mp3")) {
preview = document.createElement("audio");
preview.controls = true;
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
}
const a = document.createElement("a");
a.href = href;
a.textContent = title;
a.download = "";
li.appendChild(a);
ul.appendChild(li);
});
if (!previewAdded && links.length > 0) {
const firstHref = links[0].getAttribute("href");
if (firstHref) {
const preview = document.createElement("img");
preview.src = firstHref;
previewContainer.appendChild(preview);
}
}
storyDiv.appendChild(previewContainer);
storyDiv.appendChild(ul);
container.appendChild(storyDiv);
} else {
storyBlocks.forEach((storyBlock, index) => {
const storyDiv = document.createElement("div");
storyDiv.className = "story";
storyDiv.innerHTML = `<h3>Story #${index}</h3>`;
const previewContainer = document.createElement("div");
previewContainer.className = "preview-container";
const links = storyBlock.querySelectorAll("a.abutton");
let previewAdded = false;
const ul = document.createElement("ul");
links.forEach((link) => {
const title =
link.querySelector("span")?.textContent.trim() || "Download";
const href = link.getAttribute("href");
const li = document.createElement("li");
let preview;
if (!previewAdded && title.includes("Video/Mp4")) {
preview = document.createElement("video");
preview.controls = true;
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
} else if (!previewAdded && title.includes("Photo/Jpg")) {
preview = document.createElement("img");
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
} else if (!previewAdded && title.includes("Audio/Mp3")) {
preview = document.createElement("audio");
preview.controls = true;
preview.src = href;
previewContainer.appendChild(preview);
previewAdded = true;
}
const a = document.createElement("a");
a.href = href;
a.textContent = title;
a.download = "";
li.appendChild(a);
ul.appendChild(li);
});
if (!previewAdded) {
const thumbImg = storyBlock.querySelector(".snaptik-left img");
if (thumbImg) {
const preview = document.createElement("img");
preview.src = thumbImg.src;
previewContainer.appendChild(preview);
}
}
storyDiv.appendChild(previewContainer);
storyDiv.appendChild(ul);
container.appendChild(storyDiv);
});
}
results.style.display = "block";
status.textContent = "Stories fetched successfully!";
urlInput.value = "";
} catch (error) {
status.textContent = `Error: ${error.message}`;
status.classList.add("error");
} finally {
loader.style.display = "none";
form.querySelector('button[type="submit"]').disabled = false;
}
}
function backToStoriesForm() {
document.getElementById("stories-results").style.display = "none";
document.getElementById("storiesForm").reset();
document.getElementById("status").textContent = "";
}
async function readChunk() {
if (!currentReader || !isDownloading) return;
try {
// If paused, wait and check again
while (paused && isDownloading) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
if (!isDownloading) return;
const { done, value } = await currentReader.read();
if (done) {
// Download complete
await completeDownload();
return;
}
currentChunks.push(value);
currentReceived += value.length;
if (currentTotal > 0) {
const percent = Math.min(
(currentReceived / currentTotal) * 100,
100
).toFixed(2);
document.getElementById("progress-bar").value = percent;
document.getElementById(
"progress-text"
).textContent = `${percent}%`;
} else {
document.getElementById("progress-text").textContent = `${(
currentReceived /
1024 /
1024
).toFixed(1)} MB`;
}
if (isDownloading) {
readChunk();
}
} catch (err) {
if (isDownloading) {
throw err;
}
}
}
function togglePause() {
paused = !paused;
const pauseBtn = document.getElementById("pause-btn");
if (paused) {
pauseBtn.textContent = "Resume";
pauseBtn.classList.add("paused");
document.getElementById("status").textContent = "Download paused.";
} else {
pauseBtn.textContent = "Pause";
pauseBtn.classList.remove("paused");
document.getElementById("status").textContent =
"Streaming download...";
readChunk(); // Resume reading
}
}
async function cancelDownload() {
isDownloading = false;
paused = false;
if (currentReader) {
await currentReader.cancel();
}
document.getElementById("status").textContent = "Download cancelled.";
document.getElementById("status").classList.add("error");
resetDownloadState();
}
async function completeDownload() {
isDownloading = false;
paused = false;
const blob = new Blob(currentChunks);
let downloadName = "facebook_content";
const contentDisposition = currentResponse.headers.get(
"Content-Disposition"
);
if (contentDisposition && contentDisposition.includes("attachment")) {
const filenameRegex = /filename[^;=\n]*="?([^";\n]*)"?/;
const matches = filenameRegex.exec(contentDisposition);
if (matches != null && matches[1]) {
downloadName = decodeURIComponent(matches[1]);
}
}
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style.display = "none";
a.href = url;
a.download = downloadName;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
a.remove();
document.getElementById("status").textContent =
"Download complete! Check your browser's downloads.";
document.getElementById("progress-container").style.display = "none";
document.getElementById("control-buttons").style.display = "none";
resetDownloadState();
}
function resetDownloadState() {
document
.querySelectorAll('button[type="submit"]')
.forEach((btn) => (btn.disabled = false));
document.getElementById("spinner").style.display = "none";
document.getElementById("progress-container").style.display = "none";
document.getElementById("control-buttons").style.display = "none";
isDownloading = false;
paused = false;
currentReader = null;
currentChunks = [];
currentReceived = 0;
currentResponse = null;
}
document
.getElementById("videoForm")
.addEventListener("submit", handleSubmit);
document
.getElementById("audioForm")
.addEventListener("submit", handleSubmit);
document
.getElementById("storiesForm")
.addEventListener("submit", handleStoriesSubmit);
</script>
</body>
</html>