mirror of
https://github.com/hyzendust/Sharepoint-Downloader.git
synced 2026-06-30 20:32:20 +02:00
Merge pull request #1 from MiniduTH/feature/new-dl-logic
Feature/new dl logic
This commit is contained in:
100
README.md
100
README.md
@@ -1,73 +1,89 @@
|
|||||||
# MTH Video Manifest Capture
|
# SharePoint Video Downloader
|
||||||
|
|
||||||
A Chrome extension that captures video manifest URLs (e.g., HLS `.m3u8`, MPEG-DASH `.mpd`, and SharePoint DASH manifests) from web pages, processes them, and generates FFmpeg and yt-dlp commands for downloading.
|
A Chrome/Edge extension that captures SharePoint/OneDrive Stream video URLs and generates ready-to-run yt-dlp and FFmpeg download commands.
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
**MTH Video Manifest Capture** is designed to assist users in capturing and processing video streaming manifests, particularly for SharePoint, HLS, and DASH formats. The extension provides a user-friendly popup interface to view cleaned manifest URLs, select quality and format options, set custom filenames, and copy commands for FFmpeg and yt-dlp to download the video content.
|
**SharePoint Video Downloader** detects video manifests and stream URLs on SharePoint/OneDrive pages and generates download commands with the correct authentication flags. It supports both `--cookies-from-browser` (browser must be closed) and `--cookies cookies.txt` (browser can stay open) workflows.
|
||||||
|
|
||||||
This extension is built using Chrome's **Manifest V3**, ensuring modern security and performance standards.
|
Built using Chrome **Manifest V3**.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Captures video manifest URLs** (`.m3u8`, `.mpd`, and SharePoint `/transform/videomanifest` with `format=dash`).
|
- **Detects SharePoint stream URLs** — `/transform/videomanifest` (DASH/HLS), `mediap.svc.ms` proxy manifests, `oneDrive.transcode` segment endpoints, and direct `download.aspx` links.
|
||||||
- **Cleans URLs** by removing unnecessary parameters (e.g., after `format=dash`).
|
- **Per-tab URL capture** — stores up to 20 URLs per tab, deduplicated.
|
||||||
- **Generates customizable FFmpeg and yt-dlp commands** with options for quality (Best, 1080p, 720p, 480p) and format (MP4, MKV, TS).
|
- **Current Page shortcut** — one-click copy of a yt-dlp command using the active SharePoint tab URL (uses the built-in SharePoint extractor).
|
||||||
- **Allows custom output filenames** or uses a timestamp-based default (e.g., `video_YYYYMMDD_HHMMSS`).
|
- **Two cookie modes** — `--cookies-from-browser <browser>` and `--cookies cookies.txt`.
|
||||||
- **"Download Manifest" button** to open the cleaned manifest URL in a new tab or initiate a basic download of the manifest file.
|
- **Cookie lock warning** — shows a warning when Edge/Chrome/Brave is selected, since Chromium locks the cookie database while the browser is open.
|
||||||
- **Toggle buttons** to expand/collapse long URLs and commands for better readability.
|
- **Customizable output** — quality (Best, 1080p, 720p, 480p), format (MP4, MKV, TS), and custom filename.
|
||||||
- **Copy buttons** for FFmpeg and yt-dlp commands to easily paste into a terminal.
|
- **Copy buttons** for all commands.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### Prerequisites
|
### Prerequisites
|
||||||
|
|
||||||
- Google Chrome or Chromium browser (**Manifest V3 compatible, version 88 or later**).
|
- **Edge or Chrome** (Manifest V3, version 88+).
|
||||||
- FFmpeg and yt-dlp installed on your system (**optional, for running the generated commands**).
|
- **yt-dlp** — `pip install yt-dlp` or download from [github.com/yt-dlp/yt-dlp](https://github.com/yt-dlp/yt-dlp).
|
||||||
|
- **FFmpeg** _(optional)_ — [ffmpeg.org](https://ffmpeg.org/).
|
||||||
|
|
||||||
### Steps
|
### Steps
|
||||||
|
|
||||||
1. **Clone or Download the Repository**
|
1. **Clone or download the repository**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/MiniduTH/Sharepoint-Downloader.git
|
git clone https://github.com/MiniduTH/Sharepoint-Downloader.git
|
||||||
```
|
```
|
||||||
Or download the ZIP file and extract it.
|
|
||||||
|
|
||||||
2. **Load the Extension in Chrome**
|
2. **Load the extension**
|
||||||
- Open Chrome and navigate to `chrome://extensions/`.
|
- Edge: `edge://extensions/` → Enable **Developer mode** → **Load unpacked** → select the folder.
|
||||||
- Enable **"Developer mode"** in the top-right corner.
|
- Chrome: `chrome://extensions/` → same steps.
|
||||||
- Click **"Load unpacked"** and select the `Sharepoint-Downloader` folder.
|
|
||||||
|
|
||||||
3. **Install Dependencies** *(optional, for using generated commands)*
|
|
||||||
- **FFmpeg:** Install via your package manager (e.g., `sudo apt install ffmpeg` on Ubuntu) or download from [ffmpeg.org](https://ffmpeg.org/).
|
|
||||||
- **yt-dlp:** Install via `pip install yt-dlp` or download from [yt-dlp.org](https://yt-dlp.org/).
|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. **Open a Web Page with Video Manifests**
|
### Recommended: Current Page method
|
||||||
- Visit a website streaming video content (e.g., SharePoint, HLS, or DASH streams).
|
|
||||||
|
|
||||||
2. **Activate the Extension**
|
1. Open the SharePoint video page in your browser.
|
||||||
- Click the **"MTH Video Manifest Capture"** icon in the Chrome toolbar.
|
2. Click the extension icon.
|
||||||
|
3. In the **Current Page** card, click **Copy yt-dlp (cookies.txt)** (if Edge is open) or **Copy yt-dlp Command** (if you'll close the browser first).
|
||||||
|
4. Paste into a terminal and run.
|
||||||
|
|
||||||
3. **Capture Manifests**
|
### Cookie authentication — two options
|
||||||
- The extension automatically detects and captures video manifest URLs (`.m3u8`, `.mpd`, or SharePoint DASH manifests).
|
|
||||||
- The cleaned URL, FFmpeg command, and yt-dlp command will appear in the popup.
|
|
||||||
|
|
||||||
4. **Customize Options**
|
#### Option A: `--cookies-from-browser` (no extra setup, but browser must be closed)
|
||||||
- Use the dropdowns to select video quality (**Best, 1080p, 720p, 480p**) and format (**MP4, MKV, TS**).
|
|
||||||
- Enter a custom output filename, or leave it blank to use the timestamp-based default (e.g., `video_20250305_143022.mp4`).
|
|
||||||
|
|
||||||
5. **Copy Commands**
|
Chromium-based browsers (Edge, Chrome, Brave) **lock the cookie database while running**. You must close the browser before running the command.
|
||||||
- Click **"Copy FFmpeg"** or **"Copy yt-dlp"** to copy the respective commands to your clipboard.
|
|
||||||
- Paste the commands into a terminal to download the video using FFmpeg or yt-dlp.
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Close Edge/Chrome completely
|
||||||
|
# 2. Run:
|
||||||
|
yt-dlp --cookies-from-browser edge -f "bestvideo+bestaudio/best" --merge-output-format mp4 -o "lecture.mp4" "https://mysliit-my.sharepoint.com/..."
|
||||||
|
# 3. Reopen the browser
|
||||||
|
```
|
||||||
|
|
||||||
## Screenshots
|
Firefox does **not** lock its cookie database, so `--cookies-from-browser firefox` works while the browser is open.
|
||||||
|
|
||||||
_Screenshot of the popup interface showing captured manifest, commands, and options._
|
#### Option B: `--cookies cookies.txt` ✅ Confirmed working
|
||||||
|
|
||||||

|
Export cookies while the browser is open using the **[Get cookies.txt LOCALLY](https://chromewebstore.google.com/detail/get-cookiestxt-locally/cclelndahbckbenkjhflpdbgdldlbecc)** extension, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yt-dlp --cookies cookies.txt -f "bestvideo+bestaudio/best" --merge-output-format mp4 -o "lecture.mp4" "https://mysliit-my.sharepoint.com/..."
|
||||||
|
```
|
||||||
|
|
||||||
|
**Steps to export cookies.txt:**
|
||||||
|
|
||||||
|
1. Install **Get cookies.txt LOCALLY** in Edge/Chrome.
|
||||||
|
2. Navigate to `mysliit-my.sharepoint.com` (or your SharePoint domain).
|
||||||
|
3. Click the extension icon → **Export** → save as `cookies.txt` in the same folder where you'll run yt-dlp.
|
||||||
|
4. Use the **Copy yt-dlp (cookies.txt)** button in this extension to get the command.
|
||||||
|
|
||||||
|
### Captured Stream URLs
|
||||||
|
|
||||||
|
The extension also intercepts manifest URLs as you play the video:
|
||||||
|
|
||||||
|
1. Play the video on SharePoint.
|
||||||
|
2. Open the extension popup — captured URLs appear in the **Captured Stream URLs** list.
|
||||||
|
3. Click a URL to generate FFmpeg and yt-dlp commands for it in the **Download Commands** card.
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -94,6 +110,6 @@ This project is licensed under the **MIT License**. See the `LICENSE` file for d
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
- **Author:** Minidu Weerasinghe
|
- **Author:** Minidu Weerasinghe
|
||||||
- **GitHub:** [MiniduTH](https://github.com/MiniduTH)
|
- **GitHub:** [MiniduTH](https://github.com/MiniduTH)
|
||||||
- **LinkedIn:** [linkedin.com/in/minidu0th](https://linkedin.com/in/minidu0th)
|
- **LinkedIn:** [linkedin.com/in/minidu0th](https://linkedin.com/in/minidu0th)
|
||||||
|
|||||||
209
background.js
209
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(
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
223
popup.css
223
popup.css
@@ -1,45 +1,70 @@
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warn {
|
||||||
|
margin: 6px 0 4px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: #fff3cd;
|
||||||
|
border: 1px solid #ffc107;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #664d03;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.warn code {
|
||||||
|
background: rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── 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 +74,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;
|
||||||
|
}
|
||||||
|
|||||||
200
popup.html
200
popup.html
@@ -1,73 +1,151 @@
|
|||||||
<!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="copyTabYtdlpCookies"
|
||||||
|
class="btn secondary small"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
Copy yt-dlp (cookies.txt)
|
||||||
|
</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>
|
||||||
|
<div id="cookieWarn" class="warn" style="display: none">
|
||||||
|
⚠ <strong>Close your browser first!</strong> Chromium-based
|
||||||
|
browsers lock the cookie DB while open — yt-dlp will fail to read
|
||||||
|
cookies. Close it, run the command, then reopen.<br />
|
||||||
|
<span
|
||||||
|
>Alternative: export cookies with
|
||||||
|
<em>Get cookies.txt LOCALLY</em> extension, save as
|
||||||
|
<code>cookies.txt</code>, then use the command below.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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 (browser cookies):</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
|
||||||
|
class="manifest-section"
|
||||||
|
id="ytdlpCookiesFileSection"
|
||||||
|
style="display: none"
|
||||||
|
>
|
||||||
|
<label>yt-dlp (cookies.txt file):</label>
|
||||||
|
<div class="text-container">
|
||||||
|
<p id="ytdlpCookiesFileCommand" class="text"></p>
|
||||||
|
</div>
|
||||||
|
<button id="copyYtdlpCookiesFileBtn" class="btn secondary small">
|
||||||
|
Copy yt-dlp (cookies.txt)
|
||||||
|
</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>
|
||||||
|
|||||||
464
popup.js
464
popup.js
@@ -1,249 +1,279 @@
|
|||||||
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 copyTabYtdlpCookies = document.getElementById("copyTabYtdlpCookies");
|
||||||
const ytdlpCommandDiv = document.getElementById('ytdlpCommand');
|
const copyTabUrl = document.getElementById("copyTabUrl");
|
||||||
const refreshButton = document.getElementById('refresh');
|
const browserSelect = document.getElementById("browser");
|
||||||
const copyFffmpegBtn = document.getElementById('copyFffmpegBtn'); // Fixed typo
|
const qualitySelect = document.getElementById("quality");
|
||||||
const copyYtdlpBtn = document.getElementById('copyYtdlpBtn');
|
const formatSelect = document.getElementById("format");
|
||||||
const qualitySelect = document.getElementById('quality');
|
const filenameInput = document.getElementById("filename");
|
||||||
const formatSelect = document.getElementById('format');
|
const manifestListDiv = document.getElementById("manifestList");
|
||||||
const filenameInput = document.getElementById('filename');
|
const commandsCard = document.getElementById("commandsCard");
|
||||||
const toggleUrlBtn = document.getElementById('toggleUrl');
|
const ffmpegCommandDiv = document.getElementById("ffmpegCommand");
|
||||||
const toggleFffmpegBtn = document.getElementById('toggleFffmpeg'); // Fixed typo
|
const ytdlpCommandDiv = document.getElementById("ytdlpCommand");
|
||||||
const toggleYtdlpBtn = document.getElementById('toggleYtdlp');
|
const copyFfmpegBtn = document.getElementById("copyFfmpegBtn");
|
||||||
|
const copyYtdlpBtn = document.getElementById("copyYtdlpBtn");
|
||||||
|
const ytdlpCookiesFileSection = document.getElementById(
|
||||||
|
"ytdlpCookiesFileSection",
|
||||||
|
);
|
||||||
|
const ytdlpCookiesFileCommandDiv = document.getElementById(
|
||||||
|
"ytdlpCookiesFileCommand",
|
||||||
|
);
|
||||||
|
const copyYtdlpCookiesFileBtn = document.getElementById(
|
||||||
|
"copyYtdlpCookiesFileBtn",
|
||||||
|
);
|
||||||
|
const cookieWarn = document.getElementById("cookieWarn");
|
||||||
|
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
|
// Whether the selected browser uses a locked Chromium cookie DB
|
||||||
function generateYtdlpCommand(cleanedUrl, quality, format) {
|
function isChromiumBrowser() {
|
||||||
const userFilename = filenameInput.value.trim();
|
return ["edge", "chrome", "brave"].includes(browser());
|
||||||
const filename = userFilename || `${generateTimestampFilename()}.${format}`;
|
}
|
||||||
let qualityParam = '';
|
|
||||||
if (quality !== 'best') {
|
function updateCookieWarning() {
|
||||||
qualityParam = `--recode-video ${format} -f bestvideo[height<=?${quality.replace('p', '')}]+bestaudio/best[height<=?${quality.replace('p', '')}]`;
|
const show = isChromiumBrowser();
|
||||||
} else {
|
cookieWarn.style.display = show ? "" : "none";
|
||||||
qualityParam = `--recode-video ${format} -f bestvideo+bestaudio/best`;
|
ytdlpCookiesFileSection.style.display =
|
||||||
|
show && selectedManifest ? "" : "none";
|
||||||
|
copyTabYtdlpCookies.style.display = show ? "" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── 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 ytdlpCookiesFileForPage(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 cookies.txt -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 ytdlpCookiesFileForManifest(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 cookies.txt -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",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
copyTabYtdlpCookies.addEventListener("click", () => {
|
||||||
|
if (currentTabUrl)
|
||||||
|
copyText(
|
||||||
|
ytdlpCookiesFileForPage(currentTabUrl),
|
||||||
|
copyTabYtdlpCookies,
|
||||||
|
"Copy yt-dlp (cookies.txt)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
ytdlpCookiesFileCommandDiv.textContent = ytdlpCookiesFileForManifest(
|
||||||
);
|
selectedManifest.cleanedUrl,
|
||||||
const ytdlpCommand = generateYtdlpCommand(
|
);
|
||||||
currentManifestData.cleanedUrl,
|
updateCookieWarning();
|
||||||
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');
|
copyYtdlpCookiesFileBtn.addEventListener("click", () => {
|
||||||
toggleYtdlpBtn.textContent = ytdlpCommandDiv.classList.contains('expanded') ? 'Collapse' : 'Expand';
|
copyText(
|
||||||
}
|
ytdlpCookiesFileCommandDiv.textContent,
|
||||||
|
copyYtdlpCookiesFileBtn,
|
||||||
|
"Copy yt-dlp (cookies.txt)",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Recalculate commands when options change
|
||||||
|
[qualitySelect, formatSelect, filenameInput, browserSelect].forEach((el) => {
|
||||||
|
el.addEventListener("change", () => {
|
||||||
|
updateCommands();
|
||||||
|
updateCookieWarning();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
// Init cookie warning on load
|
||||||
|
updateCookieWarning();
|
||||||
|
|
||||||
|
// Live-listen for new manifests
|
||||||
|
chrome.runtime.onMessage.addListener((msg) => {
|
||||||
|
if (msg.type === "manifestDetected" && msg.data.tabId === currentTabId) {
|
||||||
|
loadManifests();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user