您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add a custom download button and provide options to download the video or audio directly from the YouTube page.
- // ==UserScript==
- // @name YouTube Direct Downloader
- // @description Add a custom download button and provide options to download the video or audio directly from the YouTube page.
- // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com
- // @version 1.5
- // @author afkarxyz
- // @namespace https://github.com/afkarxyz/userscripts/
- // @supportURL https://github.com/afkarxyz/userscripts/issues
- // @license MIT
- // @match https://www.youtube.com/*
- // @match https://youtube.com/*
- // @grant GM.xmlHttpRequest
- // @grant GM_download
- // @grant GM.download
- // @grant GM_setValue
- // @grant GM_getValue
- // @connect api.mp3youtube.cc
- // @connect iframe.y2meta-uk.com
- // @connect *
- // @run-at document-end
- // ==/UserScript==
- (function() {
- 'use strict';
- let lastSelectedFormat = GM_getValue('lastSelectedFormat', 'video');
- let lastSelectedVideoQuality = GM_getValue('lastSelectedVideoQuality', '1080');
- let lastSelectedAudioBitrate = GM_getValue('lastSelectedAudioBitrate', '320');
- const API_KEY_URL = 'https://api.mp3youtube.cc/v2/sanity/key';
- const API_CONVERT_URL = 'https://api.mp3youtube.cc/v2/converter';
- const REQUEST_HEADERS = {
- "Content-Type": "application/json",
- "Origin": "https://iframe.y2meta-uk.com",
- "Accept": "*/*",
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
- };
- const style = document.createElement('style');
- style.textContent = `
- .ytddl-download-btn {
- width: 36px;
- height: 36px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- margin-left: 8px;
- transition: background-color 0.2s;
- }
- html[dark] .ytddl-download-btn {
- background-color: #ffffff1a;
- }
- html:not([dark]) .ytddl-download-btn {
- background-color: #0000000d;
- }
- html[dark] .ytddl-download-btn:hover {
- background-color: #ffffff33;
- }
- html:not([dark]) .ytddl-download-btn:hover {
- background-color: #00000014;
- }
- .ytddl-download-btn svg {
- width: 18px;
- height: 18px;
- }
- html[dark] .ytddl-download-btn svg {
- fill: var(--yt-spec-text-primary, #fff);
- }
- html:not([dark]) .ytddl-download-btn svg {
- fill: var(--yt-spec-text-primary, #030303);
- }
- .ytddl-dialog {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: #000000;
- color: #e1e1e1;
- border-radius: 12px;
- box-shadow: 0 0 0 1px rgba(225,225,225,.1), 0 2px 4px 1px rgba(225,225,225,.18);
- font-family: 'IBM Plex Mono', 'Noto Sans Mono Variable', 'Noto Sans Mono', monospace;
- width: 400px;
- z-index: 9999;
- padding: 16px;
- }
- .ytddl-backdrop {
- position: fixed;
- top: 0;
- left: 0;
- width: 100%;
- height: 100%;
- background: rgba(0, 0, 0, 0.5);
- z-index: 9998;
- }
- .ytddl-dialog h3 {
- margin: 0 0 16px 0;
- font-size: 18px;
- font-weight: 700;
- }
- .quality-options {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 8px;
- margin-bottom: 16px;
- }
- .quality-option {
- display: flex;
- align-items: center;
- padding: 8px;
- cursor: pointer;
- border-radius: 6px;
- }
- .quality-option:hover {
- background: #191919;
- }
- .quality-option input[type="radio"] {
- margin-right: 8px;
- }
- .download-status {
- text-align: center;
- margin: 16px 0;
- font-size: 12px;
- display: none;
- color: #1ed760;
- }
- .button-container {
- display: flex;
- justify-content: center;
- gap: 8px;
- margin-top: 16px;
- }
- .ytddl-button {
- background: transparent;
- border: 1px solid #e1e1e1;
- color: #e1e1e1;
- font-size: 14px;
- font-weight: 500;
- padding: 8px 16px;
- border-radius: 18px;
- cursor: pointer;
- font-family: inherit;
- transition: all 0.2s;
- }
- .ytddl-button:hover {
- background: #1ed760;
- border-color: #1ed760;
- color: #000000;
- }
- .ytddl-button.cancel:hover {
- background: #f3727f;
- border-color: #f3727f;
- color: #000000;
- }
- .format-selector {
- margin-bottom: 16px;
- display: flex;
- gap: 8px;
- justify-content: center;
- }
- .format-button {
- background: transparent;
- border: 1px solid #e1e1e1;
- color: #e1e1e1;
- padding: 6px 12px;
- border-radius: 14px;
- cursor: pointer;
- font-family: inherit;
- font-size: 12px;
- transition: all 0.2s ease;
- }
- .format-button:hover {
- background: #808080;
- color: #000000;
- }
- .format-button.selected {
- background: #1ed760;
- border-color: #1ed760;
- color: #000000;
- }
- .ytddl-overlay {
- position: fixed;
- top: 20px;
- right: 20px;
- background: rgba(0, 0, 0, 0.9);
- color: #e1e1e1;
- border-radius: 8px;
- padding: 16px;
- width: 350px;
- max-width: 350px;
- z-index: 10000;
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- font-size: 14px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
- border: 1px solid rgba(255, 255, 255, 0.1);
- backdrop-filter: blur(10px);
- opacity: 0;
- transform: translateX(100%);
- transition: all 0.3s ease;
- }
- .ytddl-overlay.show {
- opacity: 1;
- transform: translateX(0);
- }
- .ytddl-overlay-content {
- line-height: 1.5;
- }
- .ytddl-overlay-status {
- margin-bottom: 8px;
- color: #1ed760;
- font-weight: 500;
- }
- .ytddl-overlay-details {
- color: #ccc;
- font-size: 13px;
- margin-bottom: 12px;
- }
- .ytddl-overlay-file-info {
- display: flex;
- justify-content: space-between;
- margin-bottom: 8px;
- font-size: 12px;
- }
- .ytddl-overlay-size {
- color: #1ed760;
- font-weight: 500;
- }
- .ytddl-overlay-speed {
- color: #ffa500;
- font-weight: 500;
- }
- .ytddl-overlay-error {
- color: #ff6b6b;
- }
- .ytddl-overlay-success {
- color: #1ed760;
- }
- `;
- document.head.appendChild(style);
- let currentOverlay = null;
- function createOverlay() {
- if (currentOverlay) {
- removeOverlay();
- } const overlay = document.createElement('div');
- overlay.className = 'ytddl-overlay';
- const content = document.createElement('div');
- content.className = 'ytddl-overlay-content';
- const status = document.createElement('div');
- status.className = 'ytddl-overlay-status';
- status.textContent = 'Initializing...';
- const details = document.createElement('div');
- details.className = 'ytddl-overlay-details';
- details.textContent = 'Preparing download request';
- const fileInfoContainer = document.createElement('div');
- fileInfoContainer.className = 'ytddl-overlay-file-info';
- const sizeElement = document.createElement('div');
- sizeElement.className = 'ytddl-overlay-size';
- sizeElement.textContent = 'Size: Calculating...';
- const speedElement = document.createElement('div');
- speedElement.className = 'ytddl-overlay-speed';
- speedElement.textContent = 'Speed: -';
- fileInfoContainer.appendChild(sizeElement);
- fileInfoContainer.appendChild(speedElement);
- content.appendChild(status);
- content.appendChild(details);
- content.appendChild(fileInfoContainer);
- overlay.appendChild(content);
- overlay.addEventListener('click', function(e) {
- if (e.target === overlay) {
- removeOverlay();
- }
- });
- document.body.appendChild(overlay);
- setTimeout(() => {
- overlay.classList.add('show');
- }, 100);
- currentOverlay = overlay;
- return overlay;
- }
- function updateOverlay(status, details, fileSize = null, downloadSpeed = null, isError = false, isSuccess = false) {
- if (!currentOverlay) return;
- const statusEl = currentOverlay.querySelector('.ytddl-overlay-status');
- const detailsEl = currentOverlay.querySelector('.ytddl-overlay-details');
- const sizeEl = currentOverlay.querySelector('.ytddl-overlay-size');
- const speedEl = currentOverlay.querySelector('.ytddl-overlay-speed');
- if (statusEl) {
- statusEl.textContent = status;
- statusEl.className = 'ytddl-overlay-status';
- if (isError) statusEl.classList.add('ytddl-overlay-error');
- if (isSuccess) statusEl.classList.add('ytddl-overlay-success');
- }
- if (detailsEl) {
- detailsEl.textContent = details;
- }
- if (sizeEl) {
- if (fileSize !== null) {
- sizeEl.textContent = `Size: ${fileSize}`;
- sizeEl.style.display = 'block';
- } else {
- sizeEl.style.display = 'none';
- }
- }
- if (speedEl) {
- if (downloadSpeed !== null) {
- speedEl.textContent = `Speed: ${downloadSpeed}`;
- speedEl.style.display = 'block';
- } else {
- speedEl.style.display = 'none';
- }
- }
- currentOverlay.offsetHeight;
- }
- function removeOverlay() {
- if (currentOverlay) {
- currentOverlay.classList.remove('show');
- setTimeout(() => {
- if (currentOverlay && currentOverlay.parentNode) {
- currentOverlay.parentNode.removeChild(currentOverlay);
- }
- currentOverlay = null;
- }, 300);
- }
- }
- function formatBytes(bytes) {
- if (bytes === 0) return '0 Bytes';
- const k = 1024;
- const sizes = ['Bytes', 'KB', 'MB', 'GB'];
- const i = Math.floor(Math.log(bytes) / Math.log(k));
- return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
- }
- function truncateTitle(title, maxLength = 50) {
- if (!title || title.length <= maxLength) return title;
- return title.substring(0, maxLength - 3) + '...';
- }
- function triggerDirectDownload(url, filename) {
- let downloadStartTime = Date.now();
- updateOverlay('Validating download URL', 'Testing download link...', null, null);
- GM.xmlHttpRequest({
- method: 'HEAD',
- url: url,
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
- },
- onload: function(response) {
- console.log('URL Test Response:', response.status, response.statusText);
- console.log('Content-Length:', response.responseHeaders.match(/content-length:\s*(\d+)/i));
- if (response.status === 200 || response.status === 206) {
- fetchAndDownload(url, filename, downloadStartTime);
- } else {
- updateOverlay(
- 'Download failed',
- `Invalid download URL (Status: ${response.status})`,
- null,
- null, true
- );
- setTimeout(removeOverlay, 2500);
- }
- },
- onerror: function(error) {
- console.error('URL validation failed:', error);
- updateOverlay(
- 'Download failed',
- 'Cannot access download URL - may be expired or invalid',
- null,
- null, true
- );
- setTimeout(removeOverlay, 2500);
- }
- });
- }
- function fetchAndDownload(url, filename, downloadStartTime) {
- updateOverlay('Starting download', 'Connecting to server...', '0 B', '0 B/s');
- console.log('=== FETCH AND DOWNLOAD ===');
- console.log('URL:', url);
- console.log('Filename:', filename);
- console.log('Method: GM.xmlHttpRequest with responseType blob');
- console.log('Start time:', new Date(downloadStartTime).toISOString());
- console.log('==========================');
- let totalSize = 0;
- let downloadedSize = 0;
- let lastUpdateTime = 0;
- const UPDATE_INTERVAL = 250;
- GM.xmlHttpRequest({
- method: 'GET',
- url: url,
- responseType: 'blob',
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Referer": "https://iframe.y2meta-uk.com/",
- "Accept": "*/*"
- }, onprogress: function(progressEvent) {
- const currentTime = Date.now();
- const elapsed = (currentTime - downloadStartTime) / 1000;
- const shouldUpdate = (currentTime - lastUpdateTime) >= UPDATE_INTERVAL ||
- (progressEvent.lengthComputable && progressEvent.loaded === progressEvent.total);
- if (progressEvent.lengthComputable) {
- totalSize = progressEvent.total;
- downloadedSize = progressEvent.loaded;
- const percentage = Math.round((downloadedSize / totalSize) * 100);
- const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
- if (shouldUpdate) {
- const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
- const speedText = `${formatBytes(speed)}/s`;
- const percentText = `${percentage}%`;
- updateOverlay(
- `Downloading ${percentText}`,
- `${filename || 'video.mp4'}`,
- sizeText,
- speedText
- );
- lastUpdateTime = currentTime;
- }
- if ((currentTime - lastUpdateTime) >= 1000 || percentage === 100) {
- console.log(`[${elapsed.toFixed(1)}s] Progress: ${percentage}% | Downloaded: ${formatBytes(downloadedSize)}/${formatBytes(totalSize)} | Speed: ${formatBytes(speed)}/s`);
- }
- } else {
- downloadedSize = progressEvent.loaded || 0;
- const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
- if (shouldUpdate) {
- const sizeText = `${formatBytes(downloadedSize)}`;
- const speedText = `${formatBytes(speed)}/s`;
- const timeText = `${elapsed.toFixed(1)}s`;
- updateOverlay(
- `Downloading...`,
- `${filename || 'video.mp4'} - ${timeText}`,
- sizeText,
- speedText
- );
- lastUpdateTime = currentTime;
- }
- if ((currentTime - lastUpdateTime) >= 1000) {
- console.log(`[${elapsed.toFixed(1)}s] Downloaded: ${formatBytes(downloadedSize)} | Speed: ${formatBytes(speed)}/s`);
- }
- }
- },
- onload: function(response) {
- console.log('Fetch completed. Response status:', response.status);
- console.log('Response type:', typeof response.response);
- console.log('Response size:', response.response?.size || 'unknown');
- if (response.status === 200 && response.response) {
- updateOverlay('Creating download file', 'Converting to downloadable file...', formatBytes(response.response.size || 0), 'Processing');
- try {
- const blob = response.response;
- const blobUrl = URL.createObjectURL(blob);
- console.log('Blob created:', blob.size, 'bytes');
- console.log('Blob URL:', blobUrl);
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = blobUrl;
- a.download = filename || 'video.mp4';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(blobUrl);
- }, 1000);
- updateOverlay(
- 'Download completed successfully!',
- `${filename || 'video.mp4'}`,
- formatBytes(blob.size),
- 'Complete',
- false,
- true
- );
- console.log('✅ Download successful via blob method');
- setTimeout(() => {
- removeOverlay();
- }, 2500);
- } catch (blobError) {
- console.error('Blob download failed:', blobError);
- updateOverlay(
- 'Blob conversion failed',
- 'Trying alternative download methods...',
- null,
- null,
- true
- );
- setTimeout(() => {
- proceedWithDownload(url, filename, downloadStartTime);
- }, 2000);
- }
- } else {
- console.error('Fetch failed with status:', response.status);
- updateOverlay(
- 'Data fetch failed',
- `Server returned status ${response.status}`,
- null,
- null,
- true
- );
- setTimeout(() => {
- proceedWithDownload(url, filename, downloadStartTime);
- }, 2000);
- }
- }, onerror: function(error) {
- console.error('GM.xmlHttpRequest fetch failed:', error);
- updateOverlay(
- 'Data fetch failed',
- 'Trying native fetch method...',
- null,
- null,
- true
- );
- setTimeout(() => {
- nativeFetchDownload(url, filename, downloadStartTime);
- }, 2000);
- },
- ontimeout: function() {
- console.error('GM.xmlHttpRequest fetch timeout');
- updateOverlay(
- 'Download timeout',
- 'Trying native fetch method...',
- null,
- null,
- true
- );
- setTimeout(() => {
- nativeFetchDownload(url, filename, downloadStartTime);
- }, 2000);
- }
- });
- }
- async function nativeFetchDownload(url, filename, downloadStartTime) {
- updateOverlay('Trying native fetch', 'Using browser fetch API...', 'Starting...', 'Native method');
- console.log('=== NATIVE FETCH DOWNLOAD ===');
- console.log('URL:', url);
- console.log('Filename:', filename);
- console.log('Method: Native fetch API with ReadableStream');
- console.log('=============================');
- try {
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Referer": "https://iframe.y2meta-uk.com/",
- "Accept": "*/*"
- }
- });
- if (!response.ok) {
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
- }
- const contentLength = response.headers.get('content-length');
- const totalSize = contentLength ? parseInt(contentLength, 10) : 0;
- console.log('Native fetch response OK. Content-Length:', totalSize);
- if (response.body && totalSize > 0) {
- const reader = response.body.getReader();
- const chunks = [];
- let downloadedSize = 0;
- updateOverlay(
- 'Downloading with native fetch',
- `Total size: ${formatBytes(totalSize)}`,
- '0%',
- 'Starting...'
- );
- while (true) {
- const { done, value } = await reader.read();
- if (done) break;
- chunks.push(value);
- downloadedSize += value.length;
- const percentage = (downloadedSize / totalSize) * 100;
- const elapsed = (Date.now() - downloadStartTime) / 1000;
- const speed = elapsed > 0 ? downloadedSize / elapsed : 0;
- const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
- const speedText = `${formatBytes(speed)}/s`;
- updateOverlay(
- 'Downloading with native fetch',
- `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
- sizeText,
- speedText
- );
- console.log(`Native fetch progress: ${Math.round(percentage)}% | ${sizeText} | ${speedText}`);
- }
- const blob = new Blob(chunks);
- console.log('Native fetch blob created from chunks:', blob.size, 'bytes');
- const blobUrl = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = blobUrl;
- a.download = filename || 'video.mp4';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(blobUrl);
- }, 1000);
- updateOverlay(
- 'Native fetch download completed!',
- `${filename || 'video.mp4'}`,
- formatBytes(blob.size),
- 'Complete',
- false,
- true
- );
- } else {
- updateOverlay('Downloading with native fetch', 'Size unknown, downloading...', 'Unknown size', 'Downloading...');
- const blob = await response.blob();
- console.log('Native fetch blob created (no progress):', blob.size, 'bytes');
- const blobUrl = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = blobUrl;
- a.download = filename || 'video.mp4';
- document.body.appendChild(a);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- URL.revokeObjectURL(blobUrl);
- }, 1000);
- updateOverlay(
- 'Native fetch download completed!',
- `${filename || 'video.mp4'}`,
- formatBytes(blob.size),
- 'Complete',
- false,
- true
- );
- }
- console.log('✅ Download successful via native fetch');
- setTimeout(() => {
- removeOverlay();
- }, 2500);
- } catch (fetchError) {
- console.error('Native fetch failed:', fetchError);
- updateOverlay(
- 'Native fetch failed',
- `Error: ${fetchError.message}`,
- null,
- null,
- true
- );
- setTimeout(() => {
- proceedWithDownload(url, filename, downloadStartTime);
- }, 2000);
- }
- }function proceedWithDownload(url, filename, downloadStartTime) {
- console.log('=== FALLBACK DOWNLOAD METHODS ===');
- console.log('GM_download (legacy):', typeof GM_download, GM_download);
- console.log('GM.download (new):', typeof GM?.download, GM?.download);
- console.log('Download URL:', url);
- console.log('Filename:', filename);
- console.log('==================================');
- updateOverlay('Opening download in new tab', 'Most reliable method for video downloads', null, null);
- try {
- const downloadWindow = window.open(url, '_blank', 'noopener,noreferrer');
- if (downloadWindow) {
- console.log('New tab opened successfully');
- updateOverlay(
- 'Download opened in new tab',
- `${truncateTitle(filename || 'video.mp4')} - Check Downloads folder`,
- 'Via new tab',
- 'Browser handling',
- false,
- true
- );
- setTimeout(() => {
- removeOverlay();
- }, 10000);
- return;
- } else {
- console.log('New tab blocked, trying GM_download');
- attemptGMDownload(url, filename, downloadStartTime);
- }
- } catch (error) {
- console.error('New tab method failed:', error);
- attemptGMDownload(url, filename, downloadStartTime);
- }
- }
- function attemptGMDownload(url, filename, downloadStartTime) {
- const gmDownloadAvailable = typeof GM_download !== 'undefined' && GM_download;
- const gmDownloadNewSyntax = typeof GM !== 'undefined' && GM.download;
- if (gmDownloadAvailable) {
- try {
- updateOverlay('Trying GM_download method', `File: ${truncateTitle(filename || 'video.mp4')}`, 'Initializing...', '-');
- console.log('Using GM_download (legacy API)');
- const downloadId = GM_download(url, filename || 'video.mp4', {
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Referer": "https://iframe.y2meta-uk.com/",
- "Accept": "*/*"
- },
- onprogress: function(progressEvent) {
- console.log('Download progress (legacy):', progressEvent);
- if (progressEvent.lengthComputable) {
- const totalSize = progressEvent.total;
- const downloadedSize = progressEvent.loaded;
- const percentage = (downloadedSize / totalSize) * 100;
- const elapsed = (Date.now() - downloadStartTime) / 1000;
- const speed = downloadedSize / elapsed;
- const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
- const speedText = `${formatBytes(speed)}/s`;
- updateOverlay(
- 'Downloading via GM_download',
- `${Math.round(percentage)}% - ${truncateTitle(filename || 'video.mp4')}`,
- sizeText,
- speedText
- );
- } else {
- const elapsed = (Date.now() - downloadStartTime) / 1000;
- updateOverlay(
- 'Downloading via GM_download',
- `${truncateTitle(filename || 'video.mp4')} - ${elapsed.toFixed(1)}s elapsed`,
- 'Size unknown',
- 'Progress...'
- );
- }
- },
- onload: function() {
- console.log('Download completed successfully (legacy)');
- updateOverlay(
- 'Download completed successfully',
- `${truncateTitle(filename || 'video.mp4')}`,
- 'Complete',
- 'Done',
- false,
- true
- );
- setTimeout(() => {
- removeOverlay();
- }, 2500);
- },
- onerror: function(error) {
- console.error('GM_download error (legacy):', error);
- updateOverlay(
- 'GM_download failed',
- 'Trying alternative download methods...',
- null,
- null,
- true
- );
- setTimeout(() => {
- fallbackDownload(url, filename);
- }, 2000);
- }
- });
- console.log('Download ID (legacy):', downloadId);
- setTimeout(() => {
- console.log('Checking if GM_download callbacks fired...');
- updateOverlay(
- 'GM_download may have CORS issues',
- 'Switching to fallback methods...',
- null,
- null,
- true
- );
- setTimeout(() => {
- fallbackDownload(url, filename);
- }, 2000);
- }, 2500);
- } catch (downloadError) {
- console.error('GM_download exception:', downloadError);
- tryNewGMDownload(url, filename, downloadStartTime);
- }
- } else if (gmDownloadNewSyntax) {
- tryNewGMDownload(url, filename, downloadStartTime);
- } else {
- console.log('No GM download APIs available, using fallback');
- fallbackDownload(url, filename);
- }
- }
- function tryNewGMDownload(url, filename, downloadStartTime) {
- try {
- updateOverlay('Trying GM.download (new API)', `File: ${filename || 'video.mp4'}`, 'Initializing...', '-');
- console.log('Using GM.download (new API)');
- GM.download(url, filename || 'video.mp4', {
- headers: {
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
- "Referer": "https://iframe.y2meta-uk.com/",
- "Accept": "*/*"
- },
- onprogress: function(progressEvent) {
- console.log('Download progress (new):', progressEvent);
- if (progressEvent.lengthComputable) {
- const totalSize = progressEvent.total;
- const downloadedSize = progressEvent.loaded;
- const percentage = (downloadedSize / totalSize) * 100;
- const elapsed = (Date.now() - downloadStartTime) / 1000;
- const speed = downloadedSize / elapsed;
- const sizeText = `${formatBytes(downloadedSize)} / ${formatBytes(totalSize)}`;
- const speedText = `${formatBytes(speed)}/s`;
- updateOverlay(
- 'Downloading via GM.download',
- `${Math.round(percentage)}% - ${filename || 'video.mp4'}`,
- sizeText,
- speedText
- );
- } else {
- const elapsed = (Date.now() - downloadStartTime) / 1000;
- updateOverlay(
- 'Downloading via GM.download',
- `${filename || 'video.mp4'} - ${elapsed.toFixed(1)}s elapsed`,
- 'Size unknown',
- 'Progress...'
- );
- }
- },
- onload: function() {
- console.log('Download completed successfully (new)');
- updateOverlay(
- 'Download completed successfully',
- `${filename || 'video.mp4'}`,
- 'Complete',
- 'Done',
- false,
- true
- );
- setTimeout(() => {
- removeOverlay();
- }, 2500);
- },
- onerror: function(error) {
- console.error('GM.download error (new):', error);
- updateOverlay(
- 'GM.download failed',
- 'Trying alternative download methods...',
- null,
- null,
- true
- );
- setTimeout(() => {
- fallbackDownload(url, filename);
- }, 2000);
- }
- }).then(downloadId => {
- console.log('Download ID (new):', downloadId);
- setTimeout(() => {
- console.log('Checking if GM.download callbacks fired...');
- updateOverlay(
- 'GM.download may have CORS issues',
- 'Switching to fallback methods...',
- null,
- null,
- true
- );
- setTimeout(() => {
- fallbackDownload(url, filename);
- }, 2000);
- }, 2500);
- }).catch(error => {
- console.error('GM.download promise error:', error);
- fallbackDownload(url, filename);
- });
- } catch (downloadError) {
- console.error('GM.download exception:', downloadError);
- fallbackDownload(url, filename);
- }
- }function fallbackDownload(url, filename) {
- updateOverlay('Using direct download methods', 'Testing browser download capabilities...', null, null);
- console.log('=== FALLBACK DOWNLOAD ===');
- console.log('URL:', url);
- console.log('Filename:', filename);
- console.log('=========================');
- try {
- const a = document.createElement('a');
- a.style.display = 'none';
- a.href = url;
- a.download = filename || 'video.mp4';
- a.target = '_blank';
- a.rel = 'noopener noreferrer';
- document.body.appendChild(a);
- updateOverlay('Method 1: Force download link', 'Creating download trigger...', null, null);
- const clickEvent = new MouseEvent('click', {
- bubbles: true,
- cancelable: true,
- view: window
- });
- a.dispatchEvent(clickEvent);
- a.click();
- setTimeout(() => {
- document.body.removeChild(a);
- updateOverlay(
- 'Download link triggered',
- `${filename || 'video.mp4'} - Check Downloads folder`,
- 'Via download link',
- 'Browser handling',
- false,
- true
- );
- setTimeout(() => {
- trySecondaryMethod(url, filename);
- }, 3000);
- }, 1000);
- } catch (error) {
- console.error('Direct link method failed:', error);
- trySecondaryMethod(url, filename);
- }
- }
- function trySecondaryMethod(url, filename) {
- updateOverlay('Method 2: Location redirect', 'Attempting direct navigation...', null, null);
- try {
- const downloadWindow = window.open('', '_blank');
- if (downloadWindow) {
- downloadWindow.location.href = url;
- updateOverlay(
- 'Download redirected to new tab',
- `${filename || 'video.mp4'} - Check new tab`,
- 'Via location redirect',
- 'Tab navigation',
- false,
- true
- );
- setTimeout(() => {
- tryFinalMethod(url, filename);
- }, 2500);
- } else {
- tryFinalMethod(url, filename);
- }
- } catch (error) {
- console.error('Secondary method failed:', error);
- tryFinalMethod(url, filename);
- }
- }
- function tryFinalMethod(url, filename) {
- updateOverlay('Method 3: Manual URL access', 'Preparing manual download option...', null, null);
- try {
- navigator.clipboard.writeText(url).then(() => {
- updateOverlay(
- 'URL copied to clipboard!',
- 'Open new tab and paste (Ctrl+L, Ctrl+V, Enter)',
- 'Clipboard ready',
- 'Manual paste',
- false,
- true
- );
- console.log('=== MANUAL DOWNLOAD URL ===');
- console.log('URL copied to clipboard. Paste in new tab:');
- console.log(url);
- console.log('Filename:', filename);
- console.log('===========================');
- }).catch(() => {
- showConsoleMethod(url, filename);
- });
- } catch (error) {
- showConsoleMethod(url, filename);
- }
- setTimeout(removeOverlay, 10000);
- }
- function showConsoleMethod(url, filename) {
- console.log('=== MANUAL DOWNLOAD URL ===');
- console.log('Copy this URL and paste in new browser tab:');
- console.log(url);
- console.log('Filename:', filename);
- console.log('===========================');
- updateOverlay(
- 'Check browser console (F12)',
- 'URL available in console for manual copy',
- 'Console method',
- 'Manual copy/paste',
- false,
- false
- );
- }
- function createDownloadDialog() {
- const dialog = document.createElement('div');
- dialog.className = 'ytddl-dialog';
- const title = document.createElement('h3');
- title.textContent = '';
- const formatSelector = document.createElement('div');
- formatSelector.className = 'format-selector';
- const videoBtn = document.createElement('button');
- videoBtn.className = `format-button ${lastSelectedFormat === 'video' ? 'selected' : ''}`;
- videoBtn.setAttribute('data-format', 'video');
- videoBtn.textContent = 'VIDEO (MP4)';
- const audioBtn = document.createElement('button');
- audioBtn.className = `format-button ${lastSelectedFormat === 'audio' ? 'selected' : ''}`;
- audioBtn.setAttribute('data-format', 'audio');
- audioBtn.textContent = 'AUDIO (MP3)';
- formatSelector.appendChild(videoBtn);
- formatSelector.appendChild(audioBtn);
- const qualityContainer = document.createElement('div');
- qualityContainer.id = 'quality-container';
- const videoQualities = document.createElement('div');
- videoQualities.className = 'quality-options';
- videoQualities.id = 'video-qualities';
- videoQualities.style.display = lastSelectedFormat === 'video' ? 'grid' : 'none';
- ['144p', '240p', '360p', '480p', '720p', '1080p'].forEach((quality, index) => {
- const option = document.createElement('div');
- option.className = 'quality-option';
- const input = document.createElement('input');
- input.type = 'radio';
- input.id = `quality-${index}`;
- input.name = 'quality';
- input.value = quality.replace('p', '');
- const label = document.createElement('label');
- label.setAttribute('for', `quality-${index}`);
- label.textContent = quality;
- label.style.fontSize = '14px';
- label.style.cursor = 'pointer';
- option.appendChild(input);
- option.appendChild(label);
- videoQualities.appendChild(option);
- option.addEventListener('click', function() {
- input.checked = true;
- GM_setValue('lastSelectedVideoQuality', input.value);
- lastSelectedVideoQuality = input.value;
- });
- });
- const defaultQuality = videoQualities.querySelector(`input[value="${lastSelectedVideoQuality}"]`);
- if (defaultQuality) {
- defaultQuality.checked = true;
- }
- const audioQualities = document.createElement('div');
- audioQualities.className = 'quality-options';
- audioQualities.id = 'audio-qualities';
- audioQualities.style.display = lastSelectedFormat === 'audio' ? 'grid' : 'none';
- ['128', '256', '320'].forEach((bitrate, index) => {
- const option = document.createElement('div');
- option.className = 'quality-option';
- const input = document.createElement('input');
- input.type = 'radio';
- input.id = `bitrate-${index}`;
- input.name = 'bitrate';
- input.value = bitrate;
- const label = document.createElement('label');
- label.setAttribute('for', `bitrate-${index}`);
- label.textContent = `${bitrate} kbps`;
- label.style.fontSize = '14px';
- label.style.cursor = 'pointer';
- option.appendChild(input);
- option.appendChild(label);
- audioQualities.appendChild(option);
- option.addEventListener('click', function() {
- input.checked = true;
- GM_setValue('lastSelectedAudioBitrate', input.value);
- lastSelectedAudioBitrate = input.value;
- });
- });
- const defaultBitrate = audioQualities.querySelector(`input[value="${lastSelectedAudioBitrate}"]`);
- if (defaultBitrate) {
- defaultBitrate.checked = true;
- }
- qualityContainer.appendChild(videoQualities);
- qualityContainer.appendChild(audioQualities);
- const downloadStatus = document.createElement('div');
- downloadStatus.className = 'download-status';
- downloadStatus.id = 'download-status';
- const buttonContainer = document.createElement('div');
- buttonContainer.className = 'button-container';
- const cancelButton = document.createElement('button');
- cancelButton.className = 'ytddl-button cancel';
- cancelButton.textContent = 'Cancel';
- const downloadButton = document.createElement('button');
- downloadButton.className = 'ytddl-button';
- downloadButton.textContent = 'Download';
- buttonContainer.appendChild(cancelButton);
- buttonContainer.appendChild(downloadButton);
- dialog.appendChild(title);
- dialog.appendChild(formatSelector);
- dialog.appendChild(qualityContainer);
- dialog.appendChild(downloadStatus);
- dialog.appendChild(buttonContainer);
- formatSelector.addEventListener('click', (e) => {
- if (e.target.classList.contains('format-button')) {
- formatSelector.querySelectorAll('.format-button').forEach(btn => {
- btn.classList.remove('selected');
- });
- e.target.classList.add('selected');
- const format = e.target.getAttribute('data-format');
- if (format === 'video') {
- videoQualities.style.display = 'grid';
- audioQualities.style.display = 'none';
- lastSelectedFormat = 'video';
- GM_setValue('lastSelectedFormat', 'video');
- } else {
- videoQualities.style.display = 'none';
- audioQualities.style.display = 'grid';
- lastSelectedFormat = 'audio';
- GM_setValue('lastSelectedFormat', 'audio');
- }
- }
- });
- const backdrop = document.createElement('div');
- backdrop.className = 'ytddl-backdrop';
- return { dialog, backdrop, cancelButton, downloadButton };
- }
- function closeDialog(dialog, backdrop) {
- if (dialog && dialog.parentNode) {
- dialog.parentNode.removeChild(dialog);
- }
- if (backdrop && backdrop.parentNode) {
- backdrop.parentNode.removeChild(backdrop);
- }
- }
- function extractVideoId(url) {
- const urlObj = new URL(url);
- const searchParams = new URLSearchParams(urlObj.search);
- return searchParams.get('v');
- } async function downloadWithMP3YouTube(videoUrl, format, quality) {
- const statusElement = document.getElementById('download-status');
- createOverlay();
- if (statusElement) {
- statusElement.style.display = 'block';
- statusElement.textContent = 'Getting API key...';
- }
- try {
- updateOverlay('Getting API key', 'Connecting to MP3YouTube API...');
- const keyResponse = await new Promise((resolve, reject) => {
- GM.xmlHttpRequest({
- method: 'GET',
- url: API_KEY_URL,
- headers: REQUEST_HEADERS,
- onload: resolve,
- onerror: reject,
- ontimeout: reject
- });
- });
- const keyData = JSON.parse(keyResponse.responseText);
- if (!keyData || !keyData.key) {
- throw new Error('Failed to get API key');
- }
- const key = keyData.key;
- updateOverlay('Processing request', `${format} (${format === 'video' ? quality + 'p' : quality + ' kbps'})`);
- if (statusElement) {
- statusElement.textContent = 'Processing download...';
- }
- let payload;
- if (format === 'video') {
- payload = {
- "link": videoUrl,
- "format": "mp4",
- "audioBitrate": "128",
- "videoQuality": quality,
- "filenameStyle": "pretty",
- "vCodec": "h264"
- };
- } else {
- payload = {
- "link": videoUrl,
- "format": "mp3",
- "audioBitrate": quality,
- "filenameStyle": "pretty"
- };
- }
- const customHeaders = {
- ...REQUEST_HEADERS,
- "key": key
- };
- updateOverlay('Converting media', 'Processing video/audio conversion...');
- const downloadResponse = await new Promise((resolve, reject) => {
- GM.xmlHttpRequest({
- method: 'POST',
- url: API_CONVERT_URL,
- headers: customHeaders,
- data: JSON.stringify(payload),
- onload: resolve,
- onerror: reject,
- ontimeout: reject
- });
- });
- const downloadInfo = JSON.parse(downloadResponse.responseText);
- if (downloadInfo.url) {
- updateOverlay('Starting download', `File: ${truncateTitle(downloadInfo.filename || `video.${format === 'video' ? 'mp4' : 'mp3'}`)}`);
- if (statusElement) {
- statusElement.textContent = 'Starting download...';
- }
- triggerDirectDownload(downloadInfo.url, downloadInfo.filename);
- return downloadInfo;
- } else {
- throw new Error('No download URL received from API');
- }} catch (error) {
- updateOverlay('Download failed', `Error: ${error.message}`, null, null, true);
- setTimeout(() => {
- removeOverlay();
- }, 4000);
- throw error;
- }
- }
- function createDownloadButton() {
- const downloadButton = document.createElement('div');
- downloadButton.className = 'ytddl-download-btn';
- const svgNS = "http://www.w3.org/2000/svg";
- const svg = document.createElementNS(svgNS, "svg");
- svg.setAttribute("viewBox", "0 0 512 512");
- const path = document.createElementNS(svgNS, "path");
- path.setAttribute("d", "M256 464c114.9 0 208-93.1 208-208c0-13.3 10.7-24 24-24s24 10.7 24 24c0 141.4-114.6 256-256 256S0 397.4 0 256c0-13.3 10.7-24 24-24s24 10.7 24 24c0 114.9 93.1 208 208 208zM377.6 232.3l-104 112c-4.5 4.9-10.9 7.7-17.6 7.7s-13-2.8-17.6-7.7l-104-112c-9-9.7-8.5-24.9 1.3-33.9s24.9-8.5 33.9 1.3L232 266.9 232 24c0-13.3 10.7-24 24-24s24 10.7 24 24l0 242.9 62.4-67.2c9-9.7 24.2-10.3 33.9-1.3s10.3 24.2 1.3 33.9z");
- svg.appendChild(path);
- downloadButton.appendChild(svg);
- downloadButton.addEventListener('click', function() {
- showDownloadDialog();
- });
- return downloadButton;
- }
- function showDownloadDialog() {
- const videoUrl = window.location.href;
- const videoId = extractVideoId(videoUrl);
- if (!videoId) {
- alert('Could not extract video ID from URL');
- return;
- }
- const { dialog, backdrop, cancelButton, downloadButton } = createDownloadDialog();
- document.body.appendChild(backdrop);
- document.body.appendChild(dialog);
- backdrop.addEventListener('click', () => {
- closeDialog(dialog, backdrop);
- });
- cancelButton.addEventListener('click', () => {
- closeDialog(dialog, backdrop);
- }); downloadButton.addEventListener('click', async () => {
- const selectedFormat = dialog.querySelector('.format-button.selected').getAttribute('data-format');
- let quality;
- if (selectedFormat === 'video') {
- const selectedQuality = dialog.querySelector('input[name="quality"]:checked');
- if (!selectedQuality) {
- alert('Please select a video quality');
- return;
- }
- quality = selectedQuality.value;
- } else {
- const selectedBitrate = dialog.querySelector('input[name="bitrate"]:checked');
- if (!selectedBitrate) {
- alert('Please select an audio bitrate');
- return;
- }
- quality = selectedBitrate.value;
- }
- GM_setValue('lastSelectedFormat', selectedFormat);
- closeDialog(dialog, backdrop);
- try {
- await downloadWithMP3YouTube(videoUrl, selectedFormat, quality);
- } catch (error) {
- console.error('Download error:', error);
- updateOverlay('Download Failed', `Error: ${error.message}`, null, null, true);
- setTimeout(removeOverlay, 2500);
- }
- });
- }
- function insertDownloadButton() {
- const targetSelector = '#owner';
- const target = document.querySelector(targetSelector);
- if (target && !document.querySelector('.ytddl-download-btn')) {
- const downloadButton = createDownloadButton();
- target.appendChild(downloadButton);
- }
- }
- const observer = new MutationObserver(() => {
- if (window.location.pathname.includes('/watch')) {
- insertDownloadButton();
- }
- });
- observer.observe(document.body, { childList: true, subtree: true });
- insertDownloadButton();
- window.addEventListener('yt-navigate-finish', () => {
- insertDownloadButton();
- });
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址