Add initial project files for MTH Video Manifest Capture extension

This commit is contained in:
Minidu Thiranjaya
2025-03-05 15:20:26 +05:30
parent aa71fe6bb9
commit 823492346a
9 changed files with 722 additions and 0 deletions

107
README.md Normal file
View File

@@ -0,0 +1,107 @@
# MTH Video Manifest Capture
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. It also includes a simple UI for customization and a button to download or open the manifest URL directly.
## 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. It also includes a **"Download Manifest"** button to open the cleaned manifest URL in a new tab or initiate a basic download.
This extension is built using Chrome's **Manifest V3**, ensuring modern security and performance standards.
## Features
- **Captures video manifest URLs** (`.m3u8`, `.mpd`, and SharePoint `/transform/videomanifest` with `format=dash`).
- **Cleans URLs** by removing unnecessary parameters (e.g., after `format=dash`).
- **Generates customizable FFmpeg and yt-dlp commands** with options for quality (Best, 1080p, 720p, 480p) and format (MP4, MKV, TS).
- **Allows custom output filenames** or uses a timestamp-based default (e.g., `video_YYYYMMDD_HHMMSS`).
- **"Download Manifest" button** to open the cleaned manifest URL in a new tab or initiate a basic download of the manifest file.
- **Toggle buttons** to expand/collapse long URLs and commands for better readability.
- **Copy buttons** for FFmpeg and yt-dlp commands to easily paste into a terminal.
## Installation
### Prerequisites
- Google Chrome or Chromium browser (**Manifest V3 compatible, version 88 or later**).
- FFmpeg and yt-dlp installed on your system (**optional, for running the generated commands**).
### Steps
1. **Clone or Download the Repository**
```bash
git clone https://github.com/MiniduTH/Sharepoint-Downloader.git
```
Or download the ZIP file and extract it.
2. **Load the Extension in Chrome**
- Open Chrome and navigate to `chrome://extensions/`.
- Enable **"Developer mode"** in the top-right corner.
- 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
1. **Open a Web Page with Video Manifests**
- Visit a website streaming video content (e.g., SharePoint, HLS, or DASH streams).
2. **Activate the Extension**
- Click the **"MTH Video Manifest Capture"** icon in the Chrome toolbar.
3. **Capture Manifests**
- 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**
- 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**
- 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.
6. **Download Manifest** *(optional)*
- Click **"Download Manifest"** to open the cleaned manifest URL in a new tab or download the manifest file (**note:** this downloads the manifest, not the full video; further processing is required using FFmpeg or yt-dlp).
## Screenshots
_Screenshot of the popup interface showing captured manifest, commands, and options._
![Screenshot](https://i.ibb.co/wZmMQ04P/Screenshot-2025-03-05-151334.png)
## Known Limitations
- **"Download Manifest" only saves the manifest file** (e.g., `.m3u8` or `.mpd`), not the full video. You need tools like **FFmpeg or yt-dlp** to process it.
- **Authenticated manifest URLs** (e.g., SharePoint temp auth tokens) may require user login or additional configuration.
- **Direct video downloads** without external tools are **not supported** due to browser security restrictions.
## Contributing
Contributions are welcome! Please follow these steps:
1. **Fork the repository**.
2. **Create a new branch** for your feature or bug fix:
```bash
git checkout -b feature/your-feature-name
```
3. **Make your changes and commit them**:
```bash
git commit -m "Add your commit message here"
```
4. **Push to the branch**:
```bash
git push origin feature/your-feature-name
```
5. **Submit a pull request** to the main branch.
## License
This project is licensed under the **MIT License**. See the `LICENSE` file for details.
## Contact
- **Author:** Minidu Weerasinghe
- **GitHub:** [MiniduTH](https://github.com/MiniduTH)
- **LinkedIn:** [linkedin.com/in/minidu0th](https://linkedin.com/in/minidu0th)

75
background.js Normal file
View File

@@ -0,0 +1,75 @@
console.log('Background script loaded');
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);
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: 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);
}
});
chrome.runtime.sendMessage({ type: 'manifestDetected', data: manifestData }, (response) => {
if (chrome.runtime.lastError) {
console.error('Error sending message:', chrome.runtime.lastError.message);
}
});
}
} catch (error) {
console.error('Error in webRequest listener:', error);
}
},
{ urls: ["<all_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
}
});

BIN
icon128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
icon16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
icon48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

25
manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"manifest_version": 3,
"name": "MTH Video Manifest Capture",
"version": "1.0",
"description": "Captures video manifest URLs.",
"permissions": [
"webRequest",
"activeTab",
"storage"
],
"host_permissions": [
"<all_urls>"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icon16.png",
"48": "icon48.png",
"128": "icon128.png"
}
}
}

193
popup.css Normal file
View File

@@ -0,0 +1,193 @@
body {
width: 350px;
padding: 0;
margin: 0;
font-family: 'Arial', sans-serif;
background-color: #f8f9fa;
color: #333;
box-sizing: border-box;
}
.container {
padding: 15px;
}
.title {
margin: 0 0 15px;
font-size: 20px;
font-weight: 600;
color: #1a73e8;
text-align: center;
}
.btn {
display: block;
padding: 10px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
transition: background-color 0.2s ease, transform 0.1s ease;
}
.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);
}
.btn.secondary {
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;
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);
}
.card {
background-color: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 15px;
margin-top: 10px;
}
.card h2 {
margin: 0 0 10px;
font-size: 16px;
color: #555;
border-bottom: 1px solid #eee;
padding-bottom: 10px;
}
.manifest-section {
margin-bottom: 15px;
}
.manifest-section label {
display: block;
margin-bottom: 5px;
font-weight: 500;
color: #666;
font-size: 14px;
}
.text-container {
position: relative;
margin-bottom: 5px;
}
.text {
margin: 0;
padding: 8px;
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 12px;
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;
}
.text.expanded {
max-height: 300px;
overflow-y: auto;
white-space: normal;
text-overflow: clip;
}
.options-section {
margin: 15px 0;
display: flex;
gap: 10px;
flex-wrap: wrap;
align-items: center;
}
.options-section label {
font-size: 14px;
color: #555;
margin-right: 5px;
}
.select, .input {
padding: 6px;
font-size: 13px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
}
.input {
width: 150px;
cursor: text;
}
.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;
}
}

73
popup.html Normal file
View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html>
<head>
<title>Video Manifest Capture</title>
<link rel="stylesheet" href="popup.css">
<!-- Optional: Add Font Awesome for icons (uncomment if using) -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> -->
</head>
<body>
<div class="container">
<h1 class="title">Video Manifest Capture</h1>
<button id="refresh" class="btn primary">
<i class="fas fa-refresh"></i> Refresh
</button>
<div class="card">
<h2>Manifest Details</h2>
<div class="manifest-section">
<label>Cleaned URL:</label>
<div class="text-container">
<p id="manifestUrl" class="text">No manifest captured yet.</p>
</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">
</div>
<div class="manifest-section">
<label>FFmpeg Command:</label>
<div class="text-container">
<p id="ffmpegCommand" class="text">FFmpeg command will appear here.</p>
</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 class="manifest-section">
<label>yt-dlp Command:</label>
<div class="text-container">
<p id="ytdlpCommand" class="text">yt-dlp command will appear here.</p>
</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>
<script src="popup.js"></script>
</body>
</html>

249
popup.js Normal file
View File

@@ -0,0 +1,249 @@
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');
// 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 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 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 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`;
}
return `yt-dlp "${cleanedUrl}" ${qualityParam} -o "${filename}"`;
}
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';
}
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);
});
} catch (error) {
console.error('Error sending message:', error);
updateUI(null);
}
}
// Initial fetch
fetchLastManifest();
// Listen for new manifests
chrome.runtime.onMessage.addListener((message) => {
if (message.type === 'manifestDetected') {
updateUI(message.data);
}
});
// Add event listeners with null checks
if (refreshButton) {
refreshButton.addEventListener('click', fetchLastManifest);
}
// 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;
}
});
}
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;
}
});
}
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;
}
});
}
// 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';
});
}
});
}
// 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';
});
}
});
}
// 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';
}
});
}
// 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';
}
});
}
// 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';
}
});
}
});