|
|
<!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"); |
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
const pauseBtn = document.getElementById("pause-btn"); |
|
|
pauseBtn.onclick = togglePause; |
|
|
pauseBtn.textContent = "Pause"; |
|
|
pauseBtn.classList.remove("paused"); |
|
|
|
|
|
|
|
|
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) { |
|
|
|
|
|
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 { |
|
|
|
|
|
while (paused && isDownloading) { |
|
|
await new Promise((resolve) => setTimeout(resolve, 100)); |
|
|
} |
|
|
|
|
|
if (!isDownloading) return; |
|
|
|
|
|
const { done, value } = await currentReader.read(); |
|
|
if (done) { |
|
|
|
|
|
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(); |
|
|
} |
|
|
} |
|
|
|
|
|
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> |
|
|
|