Refactor background logic and enhance popup interface for SharePoint Video Downloader

This commit is contained in:
Minidu Thiranjaya
2026-03-29 07:25:57 +05:30
parent cb36e67fc3
commit ae8343620b
5 changed files with 562 additions and 433 deletions

View File

@@ -1,75 +1,170 @@
console.log('Background script loaded'); console.log("Background script loaded");
// Classify and clean a URL, returning { cleanedUrl, type, label } or null
function classifyUrl(url) {
try {
const u = new URL(url);
const path = u.pathname.toLowerCase();
const host = u.hostname.toLowerCase();
// 1. SharePoint videomanifest (DASH / HLS / smooth)
if (path.includes("/transform/videomanifest")) {
let type = "dash";
if (url.includes("format=hls")) type = "hls";
return { cleanedUrl: url, type, label: "Video Manifest" };
}
// 2. Media proxy service (mediap.svc.ms) — actual stream chunks/manifests
if (host.includes("mediap.svc.ms")) {
if (path.endsWith(".m3u8") || url.includes(".m3u8")) {
return {
cleanedUrl: url.split("?")[0],
type: "hls",
label: "HLS Manifest (mediap)",
};
}
if (path.endsWith(".mpd") || url.includes(".mpd")) {
return {
cleanedUrl: url.split("?")[0],
type: "dash",
label: "DASH Manifest (mediap)",
};
}
// Catch-all for other mediap requests that look like manifests
if (path.includes("manifest") || path.includes("Manifest")) {
return {
cleanedUrl: url,
type: "dash",
label: "Stream Manifest (mediap)",
};
}
return null; // Ignore individual segment fetches
}
// 3. Standalone .m3u8 / .mpd files
if (path.endsWith(".m3u8") || path.match(/\.m3u8($|\?)/)) {
return {
cleanedUrl: url.split("?")[0],
type: "hls",
label: "HLS Manifest",
};
}
if (path.endsWith(".mpd") || path.match(/\.mpd($|\?)/)) {
return {
cleanedUrl: url.split("?")[0],
type: "dash",
label: "DASH Manifest",
};
}
// 4. SharePoint direct-download links
if (host.includes("sharepoint.com") && path.includes("download.aspx")) {
return { cleanedUrl: url, type: "direct", label: "Direct Download" };
}
// 5. SharePoint OneDrive transcode API segments (mid-playback encrypted segments)
// Strip per-segment params so all segments of the same video deduplicate to one entry.
if (
host.includes("sharepoint.com") &&
path.includes("/onedrive.transcode")
) {
const u2 = new URL(url);
// Keep only identity/auth params; drop segment-specific ones
const dropParams = new Set([
"part",
"track",
"quality",
"segmentTime",
"wsd",
"ppd",
"ppst",
"headerOffset",
"headerSize",
"correlationid",
"psi",
"ccat",
"PlaybackSessionData",
]);
for (const k of dropParams) u2.searchParams.delete(k);
return {
cleanedUrl: u2.toString(),
type: "direct",
label: "SP Transcode (use Current Page tab for yt-dlp)",
};
}
return null;
} catch {
return null;
}
}
chrome.webRequest.onBeforeRequest.addListener( chrome.webRequest.onBeforeRequest.addListener(
(details) => { (details) => {
try { try {
const originalUrl = details.url; const result = classifyUrl(details.url);
if ( if (!result) return;
(originalUrl.includes('/transform/videomanifest') && originalUrl.includes('format=dash')) ||
originalUrl.endsWith('.m3u8') ||
originalUrl.endsWith('.mpd')
) {
console.log('Video manifest detected:', originalUrl);
let cleanedUrl = originalUrl; const manifestData = {
let type = 'unknown'; originalUrl: details.url,
if (originalUrl.includes('/transform/videomanifest') && originalUrl.includes('format=dash')) { cleanedUrl: result.cleanedUrl,
const index = originalUrl.indexOf('format=dash'); type: result.type,
cleanedUrl = originalUrl.substring(0, index + 'format=dash'.length); label: result.label,
type = 'dash'; timestamp: Date.now(),
} else if (originalUrl.endsWith('.mpd')) { tabId: details.tabId,
const index = originalUrl.indexOf('.mpd') + '.mpd'.length; };
cleanedUrl = originalUrl.substring(0, index);
type = 'dash'; // Store per-tab list (keep last 20 per tab)
} else if (originalUrl.endsWith('.m3u8')) { const storageKey = `manifests_${details.tabId}`;
const index = originalUrl.indexOf('.m3u8') + '.m3u8'.length; chrome.storage.local.get(storageKey, (stored) => {
cleanedUrl = originalUrl.substring(0, index); const list = stored[storageKey] || [];
type = 'hls'; // Avoid duplicates by cleanedUrl
if (!list.some((m) => m.cleanedUrl === manifestData.cleanedUrl)) {
list.push(manifestData);
if (list.length > 20) list.shift();
chrome.storage.local.set({ [storageKey]: list });
} }
});
const manifestData = { // Also keep a global "lastManifest" for backward compat
originalUrl: originalUrl, chrome.storage.local.set({ lastManifest: manifestData });
cleanedUrl: cleanedUrl,
type: type
};
chrome.storage.local.set({ lastManifest: manifestData }, (result) => {
if (chrome.runtime.lastError) {
console.error('Error saving manifest:', chrome.runtime.lastError.message);
} else {
console.log('Manifest saved:', manifestData);
}
});
chrome.runtime.sendMessage({ type: 'manifestDetected', data: manifestData }, (response) => { chrome.runtime.sendMessage(
{ type: "manifestDetected", data: manifestData },
() => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error('Error sending message:', chrome.runtime.lastError.message); /* popup closed, ignore */
} }
}); },
} );
} catch (error) { } catch (error) {
console.error('Error in webRequest listener:', error); console.error("Error in webRequest listener:", error);
} }
}, },
{ urls: ["<all_urls>"] }, { urls: ["<all_urls>"] },
["requestBody"]
); );
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('Message received:', message); if (message.type === "getManifests") {
if (message.type === 'getLastManifest') { const tabId = message.tabId;
try { const storageKey = `manifests_${tabId}`;
chrome.storage.local.get('lastManifest', (result) => { chrome.storage.local.get([storageKey, "lastManifest"], (result) => {
if (chrome.runtime.lastError) { if (chrome.runtime.lastError) {
console.error('Error getting manifest:', chrome.runtime.lastError.message); sendResponse({ manifests: [], lastManifest: null });
sendResponse({ data: null }); } else {
} else { sendResponse({
sendResponse({ data: result.lastManifest || null }); manifests: result[storageKey] || [],
} lastManifest: result.lastManifest || null,
}); });
} catch (error) { }
console.error('Error in message listener:', error); });
sendResponse({ data: null }); return true;
}
return true; // Keep the channel open for async response
} }
}); if (message.type === "clearManifests") {
const tabId = message.tabId;
chrome.storage.local.remove(`manifests_${tabId}`, () => {
sendResponse({ ok: true });
});
return true;
}
});

View File

@@ -1,16 +1,10 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "MTH Video Manifest Capture", "name": "SharePoint Video Downloader",
"version": "1.0", "version": "2.0",
"description": "Captures video manifest URLs.", "description": "Captures SharePoint/OneDrive video stream URLs and generates download commands.",
"permissions": [ "permissions": ["webRequest", "activeTab", "tabs", "storage"],
"webRequest", "host_permissions": ["<all_urls>"],
"activeTab",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": { "background": {
"service_worker": "background.js" "service_worker": "background.js"
}, },
@@ -22,4 +16,4 @@
"128": "icon128.png" "128": "icon128.png"
} }
} }
} }

206
popup.css
View File

@@ -1,45 +1,53 @@
body { body {
width: 350px; width: 400px;
padding: 0; padding: 0;
margin: 0; margin: 0;
font-family: 'Arial', sans-serif; font-family: "Segoe UI", Arial, sans-serif;
background-color: #f8f9fa; background-color: #f8f9fa;
color: #333; color: #333;
box-sizing: border-box; box-sizing: border-box;
} }
.container { .container {
padding: 15px; padding: 12px;
} }
.title { .title {
margin: 0 0 15px; margin: 0 0 12px;
font-size: 20px; font-size: 18px;
font-weight: 600; font-weight: 600;
color: #1a73e8; color: #1a73e8;
text-align: center; text-align: center;
} }
.hint {
margin: 0 0 8px;
font-size: 12px;
color: #888;
line-height: 1.4;
}
/* ── Buttons ─────────────────────────────────────────── */
.btn { .btn {
display: block; display: inline-block;
padding: 10px; padding: 8px 14px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;
font-size: 14px; font-size: 13px;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease; transition:
background-color 0.15s,
transform 0.1s;
} }
.btn.primary { .btn.primary {
width: 100%;
background-color: #1a73e8; background-color: #1a73e8;
color: white; color: white;
} }
.btn.primary:hover { .btn.primary:hover {
background-color: #1557b0; background-color: #1557b0;
} }
.btn.primary:active { .btn.primary:active {
background-color: #0d47a1; background-color: #0d47a1;
transform: scale(0.98); transform: scale(0.98);
@@ -49,145 +57,173 @@ body {
background-color: #4caf50; background-color: #4caf50;
color: white; color: white;
} }
.btn.secondary:hover { .btn.secondary:hover {
background-color: #388e3c; background-color: #388e3c;
} }
.btn.secondary:active { .btn.secondary:active {
background-color: #2e7d32; background-color: #2e7d32;
transform: scale(0.98); transform: scale(0.98);
} }
.btn.small { .btn.small {
width: auto; padding: 5px 10px;
padding: 6px 12px;
margin-top: 5px;
font-size: 12px; font-size: 12px;
} }
.btn.toggle { .btn.toggle {
width: 100%;
background-color: #f5f5f5; background-color: #f5f5f5;
color: #1a73e8; color: #1a73e8;
margin-top: 5px;
border: 1px solid #ddd; border: 1px solid #ddd;
} }
.btn.toggle:hover { .btn.toggle:hover {
background-color: #e8f0fe; background-color: #e8f0fe;
} }
.btn.toggle:active { .btn-row {
background-color: #d9e6ff; display: flex;
transform: scale(0.98); gap: 6px;
margin-top: 6px;
} }
/* ── Cards ───────────────────────────────────────────── */
.card { .card {
background-color: white; background-color: white;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
padding: 15px; padding: 12px;
margin-top: 10px; margin-bottom: 10px;
} }
.card h2 { .card h2 {
margin: 0 0 10px; margin: 0 0 8px;
font-size: 16px; font-size: 14px;
color: #555; color: #555;
border-bottom: 1px solid #eee; border-bottom: 1px solid #eee;
padding-bottom: 10px; padding-bottom: 8px;
} }
.manifest-section { /* ── Text boxes ──────────────────────────────────────── */
margin-bottom: 15px;
}
.manifest-section label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #666;
font-size: 14px;
}
.text-container { .text-container {
position: relative; margin-bottom: 4px;
margin-bottom: 5px;
} }
.text { .text {
margin: 0; margin: 0;
padding: 8px; padding: 6px 8px;
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
font-size: 12px; font-size: 11px;
line-height: 1.4; line-height: 1.4;
word-wrap: break-word; word-break: break-all;
max-height: 100px; max-height: 60px;
overflow-y: hidden; overflow-y: auto;
white-space: nowrap;
text-overflow: ellipsis;
transition: max-height 0.3s ease, white-space 0.3s ease;
} }
.text.expanded { .text.expanded {
max-height: 300px; max-height: 200px;
overflow-y: auto;
white-space: normal;
text-overflow: clip;
} }
/* ── Options ─────────────────────────────────────────── */
.options-section { .options-section {
margin: 15px 0;
display: flex; display: flex;
gap: 10px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
} }
.options-section label { .options-section label {
font-size: 14px; font-size: 13px;
color: #555; color: #555;
margin-right: 5px;
} }
.select, .input { .select,
padding: 6px; .input {
font-size: 13px; padding: 5px;
font-size: 12px;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 4px; border-radius: 4px;
background-color: #fff; background-color: #fff;
cursor: pointer;
} }
.input { .input {
width: 150px; width: 110px;
cursor: text;
} }
.select:focus,
.select:focus, .input:focus { .input:focus {
outline: none; outline: none;
border-color: #1a73e8; border-color: #1a73e8;
box-shadow: 0 0 0 2px #e8f0fe; box-shadow: 0 0 0 2px #e8f0fe;
} }
@media (max-width: 350px) { /* ── Manifest list ───────────────────────────────────── */
body {
width: 100%; .manifest-list {
} max-height: 180px;
.container { overflow-y: auto;
padding: 10px; }
}
.card { .manifest-row {
padding: 10px; display: flex;
} align-items: center;
.input { gap: 6px;
width: 120px; padding: 5px 6px;
} border-radius: 4px;
.btn.small { cursor: pointer;
width: 100%; transition: background 0.15s;
padding: 8px; }
} .manifest-row:hover {
} background-color: #e8f0fe;
}
.manifest-row.selected {
background-color: #d2e3fc;
}
.badge {
flex-shrink: 0;
padding: 2px 6px;
border-radius: 3px;
font-size: 10px;
font-weight: 600;
color: #fff;
text-transform: uppercase;
}
.badge.dash {
background-color: #1a73e8;
}
.badge.hls {
background-color: #e8710a;
}
.badge.direct {
background-color: #4caf50;
}
.manifest-url {
font-size: 11px;
color: #444;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
min-width: 0;
}
.empty-msg {
margin: 0;
padding: 10px 0;
text-align: center;
font-size: 12px;
color: #999;
}
.manifest-section {
margin-bottom: 12px;
}
.manifest-section label {
display: block;
margin-bottom: 4px;
font-weight: 500;
color: #666;
font-size: 13px;
}

View File

@@ -1,73 +1,120 @@
<!DOCTYPE html> <!doctype html>
<html> <html>
<head> <head>
<title>Video Manifest Capture</title> <title>SharePoint Video Downloader</title>
<link rel="stylesheet" href="popup.css"> <link rel="stylesheet" href="popup.css" />
<!-- Optional: Add Font Awesome for icons (uncomment if using) --> </head>
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> --> <body>
</head> <div class="container">
<body> <h1 class="title">SharePoint Video Downloader</h1>
<div class="container">
<h1 class="title">MTH Video Manifest Capture</h1> <!-- Current Tab URL — best approach for yt-dlp -->
<div class="card">
<h2>Current Page (Recommended)</h2>
<div class="card"> <p class="hint">
<h2>Manifest Details</h2> Use the current SharePoint page URL with yt-dlp + browser cookies.
<div class="manifest-section"> This is the easiest method.
<label>Cleaned URL:</label> </p>
<div class="text-container"> <div class="text-container">
<p id="manifestUrl" class="text">No manifest captured yet.</p> <p id="tabUrl" class="text">Loading…</p>
</div>
<div class="btn-row">
<button id="copyTabYtdlp" class="btn secondary small">
Copy yt-dlp Command
</button>
<button id="copyTabUrl" class="btn toggle small">Copy URL</button>
</div> </div>
<button id="toggleUrl" class="btn toggle">Expand</button>
</div>
<div class="options-section">
<label>Quality:</label>
<select id="quality" class="select">
<option value="best">Best</option>
<option value="1080p">1080p</option>
<option value="720p">720p</option>
<option value="480p">480p</option>
</select>
<label>Format:</label>
<select id="format" class="select">
<option value="mp4">MP4</option>
<option value="mkv">MKV</option>
<option value="ts">TS</option>
</select>
<label>Output Filename:</label>
<input id="filename" type="text" class="input" placeholder="video_YYYYMMDD_HHMMSS">
<button id="refresh" class="btn primary">
<i class="fas fa-refresh"></i> Save Settings
</button>
</div> </div>
<div class="manifest-section"> <!-- Options -->
<label>FFmpeg Command:</label> <div class="card">
<div class="text-container"> <h2>Options</h2>
<p id="ffmpegCommand" class="text">FFmpeg command will appear here.</p> <div class="options-section">
<label>Browser:</label>
<select id="browser" class="select">
<option value="edge">Edge</option>
<option value="chrome">Chrome</option>
<option value="firefox">Firefox</option>
<option value="brave">Brave</option>
</select>
<label>Quality:</label>
<select id="quality" class="select">
<option value="best">Best</option>
<option value="1080p">1080p</option>
<option value="720p">720p</option>
<option value="480p">480p</option>
</select>
<label>Format:</label>
<select id="format" class="select">
<option value="mp4">MP4</option>
<option value="mkv">MKV</option>
<option value="ts">TS</option>
</select>
<label>Filename:</label>
<input
id="filename"
type="text"
class="input"
placeholder="auto (timestamp)"
/>
</div> </div>
<button id="toggleFffmpeg" class="btn toggle">Expand</button>
<button id="copyFffmpegBtn" class="btn secondary small">
<i class="fas fa-copy"></i> Copy FFmpeg
</button>
</div> </div>
<div class="manifest-section"> <!-- Captured manifests -->
<label>yt-dlp Command:</label> <div class="card">
<div class="text-container"> <h2>
<p id="ytdlpCommand" class="text">yt-dlp command will appear here.</p> Captured Stream URLs
<button
id="refresh"
class="btn toggle small"
style="display: inline; margin-left: 8px; padding: 3px 8px"
>
Refresh
</button>
<button
id="clearAll"
class="btn toggle small"
style="display: inline; margin-left: 4px; padding: 3px 8px"
>
Clear
</button>
</h2>
<div id="manifestList" class="manifest-list">
<p class="empty-msg">
Play a video on SharePoint to capture stream URLs.
</p>
</div>
</div>
<!-- Selected manifest commands -->
<div class="card" id="commandsCard" style="display: none">
<h2>Download Commands</h2>
<div class="manifest-section">
<label>FFmpeg:</label>
<div class="text-container">
<p id="ffmpegCommand" class="text">Select a captured URL above.</p>
</div>
<button id="copyFfmpegBtn" class="btn secondary small">
Copy FFmpeg
</button>
</div>
<div class="manifest-section">
<label>yt-dlp:</label>
<div class="text-container">
<p id="ytdlpCommand" class="text">Select a captured URL above.</p>
</div>
<button id="copyYtdlpBtn" class="btn secondary small">
Copy yt-dlp
</button>
</div> </div>
<button id="toggleYtdlp" class="btn toggle">Expand</button>
<button id="copyYtdlpBtn" class="btn secondary small">
<i class="fas fa-copy"></i> Copy yt-dlp
</button>
</div> </div>
</div> </div>
</div>
<script src="popup.js"></script> <script src="popup.js"></script>
</body> </body>
</html> </html>

393
popup.js
View File

@@ -1,249 +1,206 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener("DOMContentLoaded", () => {
// Get DOM elements with null checks const tabUrlDiv = document.getElementById("tabUrl");
const manifestUrlDiv = document.getElementById('manifestUrl'); const copyTabYtdlp = document.getElementById("copyTabYtdlp");
const ffmpegCommandDiv = document.getElementById('ffmpegCommand'); const copyTabUrl = document.getElementById("copyTabUrl");
const ytdlpCommandDiv = document.getElementById('ytdlpCommand'); const browserSelect = document.getElementById("browser");
const refreshButton = document.getElementById('refresh'); const qualitySelect = document.getElementById("quality");
const copyFffmpegBtn = document.getElementById('copyFffmpegBtn'); // Fixed typo const formatSelect = document.getElementById("format");
const copyYtdlpBtn = document.getElementById('copyYtdlpBtn'); const filenameInput = document.getElementById("filename");
const qualitySelect = document.getElementById('quality'); const manifestListDiv = document.getElementById("manifestList");
const formatSelect = document.getElementById('format'); const commandsCard = document.getElementById("commandsCard");
const filenameInput = document.getElementById('filename'); const ffmpegCommandDiv = document.getElementById("ffmpegCommand");
const toggleUrlBtn = document.getElementById('toggleUrl'); const ytdlpCommandDiv = document.getElementById("ytdlpCommand");
const toggleFffmpegBtn = document.getElementById('toggleFffmpeg'); // Fixed typo const copyFfmpegBtn = document.getElementById("copyFfmpegBtn");
const toggleYtdlpBtn = document.getElementById('toggleYtdlp'); const copyYtdlpBtn = document.getElementById("copyYtdlpBtn");
const refreshBtn = document.getElementById("refresh");
const clearAllBtn = document.getElementById("clearAll");
// Check if all elements exist let currentTabUrl = "";
if (!manifestUrlDiv || !ffmpegCommandDiv || !ytdlpCommandDiv || !refreshButton || !copyFffmpegBtn || let currentTabId = null;
!copyYtdlpBtn || !qualitySelect || !formatSelect || !filenameInput || !toggleUrlBtn || let selectedManifest = null;
!toggleFffmpegBtn || !toggleYtdlpBtn) {
console.error('One or more DOM elements not found in popup.html'); // ── Helpers ──────────────────────────────────────────────
return;
function ts() {
const d = new Date();
return `video_${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}_${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
}
function p(n) {
return String(n).padStart(2, "0");
} }
let currentManifestData = null; function outName() {
const v = filenameInput.value.trim();
// Function to generate a timestamp-based filename const ext = formatSelect.value;
function generateTimestampFilename() { if (v) return v.includes(".") ? v : `${v}.${ext}`;
const now = new Date(); return `${ts()}.${ext}`;
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `video_${year}${month}${day}_${hours}${minutes}${seconds}`;
} }
// Function to generate FFmpeg command function browser() {
function generateFffmpegCommand(cleanedUrl, type, quality, format) { return browserSelect.value;
const userFilename = filenameInput.value.trim();
const filename = userFilename || `${generateTimestampFilename()}.${format}`;
return `ffmpeg -i "${cleanedUrl}" -c copy ${filename}`;
} }
// Function to generate yt-dlp command // ── Command generators ───────────────────────────────────
function generateYtdlpCommand(cleanedUrl, quality, format) {
const userFilename = filenameInput.value.trim(); function ytdlpForPage(url) {
const filename = userFilename || `${generateTimestampFilename()}.${format}`; const q = qualitySelect.value;
let qualityParam = ''; const fmt = formatSelect.value;
if (quality !== 'best') { const name = outName();
qualityParam = `--recode-video ${format} -f bestvideo[height<=?${quality.replace('p', '')}]+bestaudio/best[height<=?${quality.replace('p', '')}]`; let fSel =
} else { q === "best"
qualityParam = `--recode-video ${format} -f bestvideo+bestaudio/best`; ? "bestvideo+bestaudio/best"
: `bestvideo[height<=?${q.replace("p", "")}]+bestaudio/best[height<=?${q.replace("p", "")}]`;
return `yt-dlp --cookies-from-browser ${browser()} -f "${fSel}" --merge-output-format ${fmt} -o "${name}" "${url}"`;
}
function ytdlpForManifest(url) {
const q = qualitySelect.value;
const fmt = formatSelect.value;
const name = outName();
let fSel =
q === "best"
? "bestvideo+bestaudio/best"
: `bestvideo[height<=?${q.replace("p", "")}]+bestaudio/best[height<=?${q.replace("p", "")}]`;
return `yt-dlp --cookies-from-browser ${browser()} -f "${fSel}" --merge-output-format ${fmt} -o "${name}" "${url}"`;
}
function ffmpegForManifest(url, type) {
const name = outName();
const referer = currentTabUrl
? ` -headers "Referer: ${new URL(currentTabUrl).origin}/"`
: "";
if (type === "direct") {
return `ffmpeg${referer} -i "${url}" -c copy "${name}"`;
} }
return `yt-dlp "${cleanedUrl}" ${qualityParam} -o "${filename}"`; return `ffmpeg -protocol_whitelist file,http,https,tcp,tls${referer} -i "${url}" -c copy "${name}"`;
} }
function updateUI(data) { // ── Copy helper ──────────────────────────────────────────
if (data) {
currentManifestData = data;
if (manifestUrlDiv) manifestUrlDiv.textContent = data.cleanedUrl; // Add null check
const ffmpegCommand = generateFffmpegCommand(
data.cleanedUrl,
data.type,
qualitySelect.value,
formatSelect.value
);
const ytdlpCommand = generateYtdlpCommand(
data.cleanedUrl,
qualitySelect.value,
formatSelect.value
);
if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand; // Add null check
if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand; // Add null check
if (filenameInput && !filenameInput.value.trim()) {
filenameInput.placeholder = generateTimestampFilename();
}
} else {
if (manifestUrlDiv) manifestUrlDiv.textContent = 'No manifest captured yet.';
if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = 'FFmpeg command will appear here.';
if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = 'yt-dlp command will appear here.';
if (filenameInput) {
filenameInput.value = '';
filenameInput.placeholder = 'video_YYYYMMDD_HHMMSS';
}
}
// Reset toggle states with null checks
if (manifestUrlDiv) manifestUrlDiv.classList.remove('expanded');
if (ffmpegCommandDiv) ffmpegCommandDiv.classList.remove('expanded');
if (ytdlpCommandDiv) ytdlpCommandDiv.classList.remove('expanded');
if (toggleUrlBtn) toggleUrlBtn.textContent = 'Expand';
if (toggleFffmpegBtn) toggleFffmpegBtn.textContent = 'Expand';
if (toggleYtdlpBtn) toggleYtdlpBtn.textContent = 'Expand';
}
function fetchLastManifest() { function copyText(text, btn, defaultLabel) {
try { navigator.clipboard
chrome.runtime.sendMessage({ type: 'getLastManifest' }, (response) => { .writeText(text)
if (chrome.runtime.lastError) { .then(() => {
console.error('Runtime error:', chrome.runtime.lastError.message); btn.textContent = "Copied!";
updateUI(null); setTimeout(() => (btn.textContent = defaultLabel), 1500);
return; })
} .catch(() => {
updateUI(response.data); btn.textContent = "Copy Failed";
setTimeout(() => (btn.textContent = defaultLabel), 1500);
}); });
} catch (error) {
console.error('Error sending message:', error);
updateUI(null);
}
} }
// Initial fetch // ── Tab URL section ──────────────────────────────────────
fetchLastManifest();
// Listen for new manifests chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
chrome.runtime.onMessage.addListener((message) => { if (tabs[0]) {
if (message.type === 'manifestDetected') { currentTabUrl = tabs[0].url;
updateUI(message.data); currentTabId = tabs[0].id;
tabUrlDiv.textContent = currentTabUrl;
loadManifests();
} }
}); });
// Add event listeners with null checks copyTabYtdlp.addEventListener("click", () => {
if (refreshButton) { if (currentTabUrl)
refreshButton.addEventListener('click', fetchLastManifest); copyText(
ytdlpForPage(currentTabUrl),
copyTabYtdlp,
"Copy yt-dlp Command",
);
});
copyTabUrl.addEventListener("click", () => {
if (currentTabUrl) copyText(currentTabUrl, copyTabUrl, "Copy URL");
});
// ── Manifest list ────────────────────────────────────────
function loadManifests() {
if (currentTabId == null) return;
chrome.runtime.sendMessage(
{ type: "getManifests", tabId: currentTabId },
(resp) => {
if (chrome.runtime.lastError || !resp) {
renderManifests([]);
return;
}
renderManifests(resp.manifests || []);
},
);
} }
// Update commands on quality/format/filename change with null checks function renderManifests(list) {
if (qualitySelect) { manifestListDiv.innerHTML = "";
qualitySelect.addEventListener('change', () => { if (list.length === 0) {
if (currentManifestData) { manifestListDiv.innerHTML =
const ffmpegCommand = generateFffmpegCommand( '<p class="empty-msg">Play a video on SharePoint to capture stream URLs.</p>';
currentManifestData.cleanedUrl, return;
currentManifestData.type, }
qualitySelect.value, list.forEach((m, i) => {
formatSelect.value const row = document.createElement("div");
); row.className =
const ytdlpCommand = generateYtdlpCommand( "manifest-row" +
currentManifestData.cleanedUrl, (selectedManifest && selectedManifest.cleanedUrl === m.cleanedUrl
qualitySelect.value, ? " selected"
formatSelect.value : "");
); row.innerHTML = `<span class="badge ${m.type}">${m.label || m.type.toUpperCase()}</span>
if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand; <span class="manifest-url" title="${m.cleanedUrl}">${m.cleanedUrl}</span>`;
if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand; row.addEventListener("click", () => selectManifest(m));
} manifestListDiv.appendChild(row);
}); });
} }
if (formatSelect) { function selectManifest(m) {
formatSelect.addEventListener('change', () => { selectedManifest = m;
if (currentManifestData) { commandsCard.style.display = "";
const ffmpegCommand = generateFffmpegCommand( updateCommands();
currentManifestData.cleanedUrl, // Re-render to highlight selection
currentManifestData.type, loadManifests();
qualitySelect.value,
formatSelect.value
);
const ytdlpCommand = generateYtdlpCommand(
currentManifestData.cleanedUrl,
qualitySelect.value,
formatSelect.value
);
if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand;
if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand;
}
});
} }
if (filenameInput) { function updateCommands() {
filenameInput.addEventListener('change', () => { if (!selectedManifest) return;
if (currentManifestData) { ffmpegCommandDiv.textContent = ffmpegForManifest(
const ffmpegCommand = generateFffmpegCommand( selectedManifest.cleanedUrl,
currentManifestData.cleanedUrl, selectedManifest.type,
currentManifestData.type, );
qualitySelect.value, ytdlpCommandDiv.textContent = ytdlpForManifest(selectedManifest.cleanedUrl);
formatSelect.value
);
const ytdlpCommand = generateYtdlpCommand(
currentManifestData.cleanedUrl,
qualitySelect.value,
formatSelect.value
);
if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand;
if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand;
}
});
} }
// Copy FFmpeg button with null check // ── Buttons ──────────────────────────────────────────────
if (copyFffmpegBtn) {
copyFffmpegBtn.addEventListener('click', () => {
if (ffmpegCommandDiv) {
const commandText = ffmpegCommandDiv.textContent;
navigator.clipboard.writeText(commandText).then(() => {
copyFffmpegBtn.textContent = 'Copied!';
setTimeout(() => copyFffmpegBtn.textContent = 'Copy FFmpeg', 2000);
}).catch((err) => {
console.error('Failed to copy FFmpeg:', err);
copyFffmpegBtn.textContent = 'Copy Failed';
});
}
});
}
// Copy yt-dlp button with null check refreshBtn.addEventListener("click", loadManifests);
if (copyYtdlpBtn) {
copyYtdlpBtn.addEventListener('click', () => {
if (ytdlpCommandDiv) {
const commandText = ytdlpCommandDiv.textContent;
navigator.clipboard.writeText(commandText).then(() => {
copyYtdlpBtn.textContent = 'Copied!';
setTimeout(() => copyYtdlpBtn.textContent = 'Copy yt-dlp', 2000);
}).catch((err) => {
console.error('Failed to copy yt-dlp:', err);
copyYtdlpBtn.textContent = 'Copy Failed';
});
}
});
}
// Toggle URL visibility with null check clearAllBtn.addEventListener("click", () => {
if (toggleUrlBtn) { if (currentTabId == null) return;
toggleUrlBtn.addEventListener('click', () => { chrome.runtime.sendMessage(
if (manifestUrlDiv) { { type: "clearManifests", tabId: currentTabId },
manifestUrlDiv.classList.toggle('expanded'); () => {
toggleUrlBtn.textContent = manifestUrlDiv.classList.contains('expanded') ? 'Collapse' : 'Expand'; selectedManifest = null;
} commandsCard.style.display = "none";
}); loadManifests();
} },
);
});
// Toggle FFmpeg visibility with null check copyFfmpegBtn.addEventListener("click", () => {
if (toggleFffmpegBtn) { copyText(ffmpegCommandDiv.textContent, copyFfmpegBtn, "Copy FFmpeg");
toggleFffmpegBtn.addEventListener('click', () => { });
if (ffmpegCommandDiv) {
ffmpegCommandDiv.classList.toggle('expanded');
toggleFffmpegBtn.textContent = ffmpegCommandDiv.classList.contains('expanded') ? 'Collapse' : 'Expand';
}
});
}
// Toggle yt-dlp visibility with null check copyYtdlpBtn.addEventListener("click", () => {
if (toggleYtdlpBtn) { copyText(ytdlpCommandDiv.textContent, copyYtdlpBtn, "Copy yt-dlp");
toggleYtdlpBtn.addEventListener('click', () => { });
if (ytdlpCommandDiv) {
ytdlpCommandDiv.classList.toggle('expanded'); // Recalculate commands when options change
toggleYtdlpBtn.textContent = ytdlpCommandDiv.classList.contains('expanded') ? 'Collapse' : 'Expand'; [qualitySelect, formatSelect, filenameInput, browserSelect].forEach((el) => {
} el.addEventListener("change", updateCommands);
}); });
}
}); // Live-listen for new manifests
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "manifestDetected" && msg.data.tabId === currentTabId) {
loadManifests();
}
});
});