diff --git a/background.js b/background.js index 76606ce..7bf286c 100644 --- a/background.js +++ b/background.js @@ -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( (details) => { try { - const originalUrl = details.url; - if ( - (originalUrl.includes('/transform/videomanifest') && originalUrl.includes('format=dash')) || - originalUrl.endsWith('.m3u8') || - originalUrl.endsWith('.mpd') - ) { - console.log('Video manifest detected:', originalUrl); + const result = classifyUrl(details.url); + if (!result) return; - let cleanedUrl = originalUrl; - let type = 'unknown'; - if (originalUrl.includes('/transform/videomanifest') && originalUrl.includes('format=dash')) { - const index = originalUrl.indexOf('format=dash'); - cleanedUrl = originalUrl.substring(0, index + 'format=dash'.length); - type = 'dash'; - } else if (originalUrl.endsWith('.mpd')) { - const index = originalUrl.indexOf('.mpd') + '.mpd'.length; - cleanedUrl = originalUrl.substring(0, index); - type = 'dash'; - } else if (originalUrl.endsWith('.m3u8')) { - const index = originalUrl.indexOf('.m3u8') + '.m3u8'.length; - cleanedUrl = originalUrl.substring(0, index); - type = 'hls'; + const manifestData = { + originalUrl: details.url, + cleanedUrl: result.cleanedUrl, + type: result.type, + label: result.label, + timestamp: Date.now(), + tabId: details.tabId, + }; + + // Store per-tab list (keep last 20 per tab) + const storageKey = `manifests_${details.tabId}`; + chrome.storage.local.get(storageKey, (stored) => { + const list = stored[storageKey] || []; + // 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 = { - originalUrl: originalUrl, - 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); - } - }); + // Also keep a global "lastManifest" for backward compat + chrome.storage.local.set({ lastManifest: manifestData }); - chrome.runtime.sendMessage({ type: 'manifestDetected', data: manifestData }, (response) => { + chrome.runtime.sendMessage( + { type: "manifestDetected", data: manifestData }, + () => { if (chrome.runtime.lastError) { - console.error('Error sending message:', chrome.runtime.lastError.message); + /* popup closed, ignore */ } - }); - } + }, + ); } catch (error) { - console.error('Error in webRequest listener:', error); + console.error("Error in webRequest listener:", error); } }, { urls: [""] }, - ["requestBody"] ); chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { - console.log('Message received:', message); - if (message.type === 'getLastManifest') { - try { - chrome.storage.local.get('lastManifest', (result) => { - if (chrome.runtime.lastError) { - console.error('Error getting manifest:', chrome.runtime.lastError.message); - sendResponse({ data: null }); - } else { - sendResponse({ data: result.lastManifest || null }); - } - }); - } catch (error) { - console.error('Error in message listener:', error); - sendResponse({ data: null }); - } - return true; // Keep the channel open for async response + if (message.type === "getManifests") { + const tabId = message.tabId; + const storageKey = `manifests_${tabId}`; + chrome.storage.local.get([storageKey, "lastManifest"], (result) => { + if (chrome.runtime.lastError) { + sendResponse({ manifests: [], lastManifest: null }); + } else { + sendResponse({ + manifests: result[storageKey] || [], + lastManifest: result.lastManifest || null, + }); + } + }); + return true; } -}); \ No newline at end of file + if (message.type === "clearManifests") { + const tabId = message.tabId; + chrome.storage.local.remove(`manifests_${tabId}`, () => { + sendResponse({ ok: true }); + }); + return true; + } +}); diff --git a/manifest.json b/manifest.json index d6cdee1..10c9c68 100644 --- a/manifest.json +++ b/manifest.json @@ -1,16 +1,10 @@ { "manifest_version": 3, - "name": "MTH Video Manifest Capture", - "version": "1.0", - "description": "Captures video manifest URLs.", - "permissions": [ - "webRequest", - "activeTab", - "storage" - ], - "host_permissions": [ - "" - ], + "name": "SharePoint Video Downloader", + "version": "2.0", + "description": "Captures SharePoint/OneDrive video stream URLs and generates download commands.", + "permissions": ["webRequest", "activeTab", "tabs", "storage"], + "host_permissions": [""], "background": { "service_worker": "background.js" }, @@ -22,4 +16,4 @@ "128": "icon128.png" } } -} \ No newline at end of file +} diff --git a/popup.css b/popup.css index 689fe9e..04b7d28 100644 --- a/popup.css +++ b/popup.css @@ -1,45 +1,53 @@ body { - width: 350px; + width: 400px; padding: 0; margin: 0; - font-family: 'Arial', sans-serif; + font-family: "Segoe UI", Arial, sans-serif; background-color: #f8f9fa; color: #333; box-sizing: border-box; } .container { - padding: 15px; + padding: 12px; } .title { - margin: 0 0 15px; - font-size: 20px; + margin: 0 0 12px; + font-size: 18px; font-weight: 600; color: #1a73e8; text-align: center; } +.hint { + margin: 0 0 8px; + font-size: 12px; + color: #888; + line-height: 1.4; +} + +/* ── Buttons ─────────────────────────────────────────── */ + .btn { - display: block; - padding: 10px; + display: inline-block; + padding: 8px 14px; border: none; border-radius: 4px; - font-size: 14px; + font-size: 13px; cursor: pointer; - transition: background-color 0.2s ease, transform 0.1s ease; + transition: + background-color 0.15s, + transform 0.1s; } .btn.primary { - width: 100%; background-color: #1a73e8; color: white; } - .btn.primary:hover { background-color: #1557b0; } - .btn.primary:active { background-color: #0d47a1; transform: scale(0.98); @@ -49,145 +57,173 @@ body { background-color: #4caf50; color: white; } - .btn.secondary:hover { background-color: #388e3c; } - .btn.secondary:active { background-color: #2e7d32; transform: scale(0.98); } .btn.small { - width: auto; - padding: 6px 12px; - margin-top: 5px; + padding: 5px 10px; font-size: 12px; } .btn.toggle { - width: 100%; background-color: #f5f5f5; color: #1a73e8; - margin-top: 5px; border: 1px solid #ddd; } - .btn.toggle:hover { background-color: #e8f0fe; } -.btn.toggle:active { - background-color: #d9e6ff; - transform: scale(0.98); +.btn-row { + display: flex; + gap: 6px; + margin-top: 6px; } +/* ── Cards ───────────────────────────────────────────── */ + .card { background-color: white; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - padding: 15px; - margin-top: 10px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 12px; + margin-bottom: 10px; } .card h2 { - margin: 0 0 10px; - font-size: 16px; + margin: 0 0 8px; + font-size: 14px; color: #555; border-bottom: 1px solid #eee; - padding-bottom: 10px; + padding-bottom: 8px; } -.manifest-section { - margin-bottom: 15px; -} - -.manifest-section label { - display: block; - margin-bottom: 5px; - font-weight: 500; - color: #666; - font-size: 14px; -} +/* ── Text boxes ──────────────────────────────────────── */ .text-container { - position: relative; - margin-bottom: 5px; + margin-bottom: 4px; } .text { margin: 0; - padding: 8px; + padding: 6px 8px; background-color: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; - font-size: 12px; + font-size: 11px; line-height: 1.4; - word-wrap: break-word; - max-height: 100px; - overflow-y: hidden; - white-space: nowrap; - text-overflow: ellipsis; - transition: max-height 0.3s ease, white-space 0.3s ease; + word-break: break-all; + max-height: 60px; + overflow-y: auto; } .text.expanded { - max-height: 300px; - overflow-y: auto; - white-space: normal; - text-overflow: clip; + max-height: 200px; } +/* ── Options ─────────────────────────────────────────── */ + .options-section { - margin: 15px 0; display: flex; - gap: 10px; + gap: 8px; flex-wrap: wrap; align-items: center; } .options-section label { - font-size: 14px; + font-size: 13px; color: #555; - margin-right: 5px; } -.select, .input { - padding: 6px; - font-size: 13px; +.select, +.input { + padding: 5px; + font-size: 12px; border: 1px solid #ddd; border-radius: 4px; background-color: #fff; - cursor: pointer; } - .input { - width: 150px; - cursor: text; + width: 110px; } - -.select:focus, .input:focus { +.select:focus, +.input:focus { outline: none; border-color: #1a73e8; box-shadow: 0 0 0 2px #e8f0fe; } -@media (max-width: 350px) { - body { - width: 100%; - } - .container { - padding: 10px; - } - .card { - padding: 10px; - } - .input { - width: 120px; - } - .btn.small { - width: 100%; - padding: 8px; - } -} \ No newline at end of file +/* ── Manifest list ───────────────────────────────────── */ + +.manifest-list { + max-height: 180px; + overflow-y: auto; +} + +.manifest-row { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 6px; + border-radius: 4px; + cursor: pointer; + transition: background 0.15s; +} +.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; +} diff --git a/popup.html b/popup.html index 6b72107..e5d09ac 100644 --- a/popup.html +++ b/popup.html @@ -1,73 +1,120 @@ - + - - Video Manifest Capture - - - - - -
-

MTH Video Manifest Capture

- - -
-

Manifest Details

-
- + + SharePoint Video Downloader + + + +
+

SharePoint Video Downloader

+ + +
+

Current Page (Recommended)

+

+ Use the current SharePoint page URL with yt-dlp + browser cookies. + This is the easiest method. +

-

No manifest captured yet.

+

Loading…

+
+
+ +
- -
- -
- - - - - - - - -
-
- -
-

FFmpeg command will appear here.

+ +
+

Options

+
+ + + + + + + + + + +
- -
-
- -
-

yt-dlp command will appear here.

+ +
+

+ Captured Stream URLs + + +

+
+

+ Play a video on SharePoint to capture stream URLs. +

+
+
+ + +
-
- - - \ No newline at end of file + + + diff --git a/popup.js b/popup.js index 9020e54..8273820 100644 --- a/popup.js +++ b/popup.js @@ -1,249 +1,206 @@ -document.addEventListener('DOMContentLoaded', () => { - // Get DOM elements with null checks - const manifestUrlDiv = document.getElementById('manifestUrl'); - const ffmpegCommandDiv = document.getElementById('ffmpegCommand'); - const ytdlpCommandDiv = document.getElementById('ytdlpCommand'); - const refreshButton = document.getElementById('refresh'); - const copyFffmpegBtn = document.getElementById('copyFffmpegBtn'); // Fixed typo - const copyYtdlpBtn = document.getElementById('copyYtdlpBtn'); - const qualitySelect = document.getElementById('quality'); - const formatSelect = document.getElementById('format'); - const filenameInput = document.getElementById('filename'); - const toggleUrlBtn = document.getElementById('toggleUrl'); - const toggleFffmpegBtn = document.getElementById('toggleFffmpeg'); // Fixed typo - const toggleYtdlpBtn = document.getElementById('toggleYtdlp'); +document.addEventListener("DOMContentLoaded", () => { + const tabUrlDiv = document.getElementById("tabUrl"); + const copyTabYtdlp = document.getElementById("copyTabYtdlp"); + const copyTabUrl = document.getElementById("copyTabUrl"); + const browserSelect = document.getElementById("browser"); + const qualitySelect = document.getElementById("quality"); + const formatSelect = document.getElementById("format"); + const filenameInput = document.getElementById("filename"); + const manifestListDiv = document.getElementById("manifestList"); + const commandsCard = document.getElementById("commandsCard"); + const ffmpegCommandDiv = document.getElementById("ffmpegCommand"); + const ytdlpCommandDiv = document.getElementById("ytdlpCommand"); + const copyFfmpegBtn = document.getElementById("copyFfmpegBtn"); + const copyYtdlpBtn = document.getElementById("copyYtdlpBtn"); + const refreshBtn = document.getElementById("refresh"); + const clearAllBtn = document.getElementById("clearAll"); - // Check if all elements exist - if (!manifestUrlDiv || !ffmpegCommandDiv || !ytdlpCommandDiv || !refreshButton || !copyFffmpegBtn || - !copyYtdlpBtn || !qualitySelect || !formatSelect || !filenameInput || !toggleUrlBtn || - !toggleFffmpegBtn || !toggleYtdlpBtn) { - console.error('One or more DOM elements not found in popup.html'); - return; + let currentTabUrl = ""; + let currentTabId = null; + let selectedManifest = null; + + // ── Helpers ────────────────────────────────────────────── + + 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 to generate a timestamp-based filename - function generateTimestampFilename() { - const now = new Date(); - 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 outName() { + const v = filenameInput.value.trim(); + const ext = formatSelect.value; + if (v) return v.includes(".") ? v : `${v}.${ext}`; + return `${ts()}.${ext}`; } - // Function to generate FFmpeg command - function generateFffmpegCommand(cleanedUrl, type, quality, format) { - const userFilename = filenameInput.value.trim(); - const filename = userFilename || `${generateTimestampFilename()}.${format}`; - return `ffmpeg -i "${cleanedUrl}" -c copy ${filename}`; + function browser() { + return browserSelect.value; } - // Function to generate yt-dlp command - function generateYtdlpCommand(cleanedUrl, quality, format) { - const userFilename = filenameInput.value.trim(); - const filename = userFilename || `${generateTimestampFilename()}.${format}`; - let qualityParam = ''; - if (quality !== 'best') { - qualityParam = `--recode-video ${format} -f bestvideo[height<=?${quality.replace('p', '')}]+bestaudio/best[height<=?${quality.replace('p', '')}]`; - } else { - qualityParam = `--recode-video ${format} -f bestvideo+bestaudio/best`; + // ── Command generators ─────────────────────────────────── + + function ytdlpForPage(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 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) { - 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'; - } + // ── Copy helper ────────────────────────────────────────── - function fetchLastManifest() { - try { - chrome.runtime.sendMessage({ type: 'getLastManifest' }, (response) => { - if (chrome.runtime.lastError) { - console.error('Runtime error:', chrome.runtime.lastError.message); - updateUI(null); - return; - } - updateUI(response.data); + function copyText(text, btn, defaultLabel) { + navigator.clipboard + .writeText(text) + .then(() => { + btn.textContent = "Copied!"; + setTimeout(() => (btn.textContent = defaultLabel), 1500); + }) + .catch(() => { + btn.textContent = "Copy Failed"; + setTimeout(() => (btn.textContent = defaultLabel), 1500); }); - } catch (error) { - console.error('Error sending message:', error); - updateUI(null); - } } - // Initial fetch - fetchLastManifest(); + // ── Tab URL section ────────────────────────────────────── - // Listen for new manifests - chrome.runtime.onMessage.addListener((message) => { - if (message.type === 'manifestDetected') { - updateUI(message.data); + chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { + if (tabs[0]) { + currentTabUrl = tabs[0].url; + currentTabId = tabs[0].id; + tabUrlDiv.textContent = currentTabUrl; + loadManifests(); } }); - // Add event listeners with null checks - if (refreshButton) { - refreshButton.addEventListener('click', fetchLastManifest); + copyTabYtdlp.addEventListener("click", () => { + if (currentTabUrl) + 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 - if (qualitySelect) { - qualitySelect.addEventListener('change', () => { - if (currentManifestData) { - const ffmpegCommand = generateFffmpegCommand( - currentManifestData.cleanedUrl, - currentManifestData.type, - qualitySelect.value, - formatSelect.value - ); - const ytdlpCommand = generateYtdlpCommand( - currentManifestData.cleanedUrl, - qualitySelect.value, - formatSelect.value - ); - if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand; - if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand; - } + function renderManifests(list) { + manifestListDiv.innerHTML = ""; + if (list.length === 0) { + manifestListDiv.innerHTML = + '

Play a video on SharePoint to capture stream URLs.

'; + return; + } + list.forEach((m, i) => { + const row = document.createElement("div"); + row.className = + "manifest-row" + + (selectedManifest && selectedManifest.cleanedUrl === m.cleanedUrl + ? " selected" + : ""); + row.innerHTML = `${m.label || m.type.toUpperCase()} + ${m.cleanedUrl}`; + row.addEventListener("click", () => selectManifest(m)); + manifestListDiv.appendChild(row); }); } - if (formatSelect) { - formatSelect.addEventListener('change', () => { - if (currentManifestData) { - const ffmpegCommand = generateFffmpegCommand( - currentManifestData.cleanedUrl, - currentManifestData.type, - qualitySelect.value, - formatSelect.value - ); - const ytdlpCommand = generateYtdlpCommand( - currentManifestData.cleanedUrl, - qualitySelect.value, - formatSelect.value - ); - if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand; - if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand; - } - }); + function selectManifest(m) { + selectedManifest = m; + commandsCard.style.display = ""; + updateCommands(); + // Re-render to highlight selection + loadManifests(); } - if (filenameInput) { - filenameInput.addEventListener('change', () => { - if (currentManifestData) { - const ffmpegCommand = generateFffmpegCommand( - currentManifestData.cleanedUrl, - currentManifestData.type, - qualitySelect.value, - formatSelect.value - ); - const ytdlpCommand = generateYtdlpCommand( - currentManifestData.cleanedUrl, - qualitySelect.value, - formatSelect.value - ); - if (ffmpegCommandDiv) ffmpegCommandDiv.textContent = ffmpegCommand; - if (ytdlpCommandDiv) ytdlpCommandDiv.textContent = ytdlpCommand; - } - }); + function updateCommands() { + if (!selectedManifest) return; + ffmpegCommandDiv.textContent = ffmpegForManifest( + selectedManifest.cleanedUrl, + selectedManifest.type, + ); + ytdlpCommandDiv.textContent = ytdlpForManifest(selectedManifest.cleanedUrl); } - // Copy FFmpeg button with null check - 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'; - }); - } - }); - } + // ── Buttons ────────────────────────────────────────────── - // Copy yt-dlp button with null check - 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'; - }); - } - }); - } + refreshBtn.addEventListener("click", loadManifests); - // Toggle URL visibility with null check - if (toggleUrlBtn) { - toggleUrlBtn.addEventListener('click', () => { - if (manifestUrlDiv) { - manifestUrlDiv.classList.toggle('expanded'); - toggleUrlBtn.textContent = manifestUrlDiv.classList.contains('expanded') ? 'Collapse' : 'Expand'; - } - }); - } + clearAllBtn.addEventListener("click", () => { + if (currentTabId == null) return; + chrome.runtime.sendMessage( + { type: "clearManifests", tabId: currentTabId }, + () => { + selectedManifest = null; + commandsCard.style.display = "none"; + loadManifests(); + }, + ); + }); - // Toggle FFmpeg visibility with null check - if (toggleFffmpegBtn) { - toggleFffmpegBtn.addEventListener('click', () => { - if (ffmpegCommandDiv) { - ffmpegCommandDiv.classList.toggle('expanded'); - toggleFffmpegBtn.textContent = ffmpegCommandDiv.classList.contains('expanded') ? 'Collapse' : 'Expand'; - } - }); - } + copyFfmpegBtn.addEventListener("click", () => { + copyText(ffmpegCommandDiv.textContent, copyFfmpegBtn, "Copy FFmpeg"); + }); - // Toggle yt-dlp visibility with null check - if (toggleYtdlpBtn) { - toggleYtdlpBtn.addEventListener('click', () => { - if (ytdlpCommandDiv) { - ytdlpCommandDiv.classList.toggle('expanded'); - toggleYtdlpBtn.textContent = ytdlpCommandDiv.classList.contains('expanded') ? 'Collapse' : 'Expand'; - } - }); - } -}); \ No newline at end of file + copyYtdlpBtn.addEventListener("click", () => { + copyText(ytdlpCommandDiv.textContent, copyYtdlpBtn, "Copy yt-dlp"); + }); + + // Recalculate commands when options change + [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(); + } + }); +});