mirror of
https://github.com/hyzendust/Sharepoint-Downloader.git
synced 2026-07-01 04:42:20 +02:00
Add initial project files for MTH Video Manifest Capture extension
This commit is contained in:
107
README.md
Normal file
107
README.md
Normal 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._
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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
75
background.js
Normal 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
BIN
icon128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
icon16.png
Normal file
BIN
icon16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
BIN
icon48.png
Normal file
BIN
icon48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
25
manifest.json
Normal file
25
manifest.json
Normal 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
193
popup.css
Normal 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
73
popup.html
Normal 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
249
popup.js
Normal 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user