YouTube Advanced Downloader

Advanced YouTube video downloader with quality selection and progress tracking

目前为 2025-02-23 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Advanced Downloader
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.1
  5. // @description Advanced YouTube video downloader with quality selection and progress tracking
  6. // @author Anassk
  7. // @match https://www.youtube.com/*
  8. // @match https://www.youtube.com/watch?v=*
  9. // @grant GM_registerMenuCommand
  10. // @grant GM_xmlhttpRequest
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_notification
  14. // @connect *
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. (function() {
  19. 'use strict';
  20.  
  21. // Configuration with storage
  22. let API_KEY = GM_getValue('API_KEY', 'Your API Key');
  23. let API_BASE = GM_getValue('API_BASE', 'Your API Base URL');
  24.  
  25. // Define qualities
  26. const QUALITIES = [
  27. { label: 'Audio Only (M4A)', value: 'audio' },
  28. { label: '144p', value: '144p' },
  29. { label: '240p', value: '240p' },
  30. { label: '360p', value: '360p' },
  31. { label: '480p', value: '480p' },
  32. { label: '720p', value: '720p' },
  33. { label: '1080p', value: '1080p' },
  34. { label: 'Highest Quality', value: 'highest' }
  35. ];
  36.  
  37. // Configuration UI
  38. function showConfig() {
  39. // Create dialog styles if not exists
  40. let style = document.getElementById('yt-dl-config-style');
  41. if (!style) {
  42. style = document.createElement('style');
  43. style.id = 'yt-dl-config-style';
  44. style.textContent = `
  45. .yt-dl-config-dialog {
  46. position: fixed;
  47. top: 50%;
  48. left: 50%;
  49. transform: translate(-50%, -50%);
  50. background: #1a1a1a;
  51. color: white;
  52. padding: 20px;
  53. border-radius: 8px;
  54. z-index: 10000;
  55. width: 400px;
  56. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  57. border: 1px solid #333;
  58. }
  59. .yt-dl-config-dialog h2 {
  60. margin: 0 0 15px 0;
  61. font-size: 18px;
  62. color: #fff;
  63. }
  64. .yt-dl-config-dialog .input-group {
  65. margin-bottom: 15px;
  66. }
  67. .yt-dl-config-dialog label {
  68. display: block;
  69. margin-bottom: 5px;
  70. color: #aaa;
  71. }
  72. .yt-dl-config-dialog input {
  73. width: 100%;
  74. padding: 8px;
  75. background: #333;
  76. color: white;
  77. border: 1px solid #444;
  78. border-radius: 4px;
  79. margin-bottom: 10px;
  80. }
  81. .yt-dl-config-dialog .buttons {
  82. display: flex;
  83. justify-content: flex-end;
  84. gap: 10px;
  85. }
  86. .yt-dl-config-dialog button {
  87. padding: 8px 16px;
  88. border: none;
  89. border-radius: 4px;
  90. cursor: pointer;
  91. }
  92. .yt-dl-config-dialog .save-btn {
  93. background: #2196F3;
  94. color: white;
  95. }
  96. .yt-dl-config-dialog .cancel-btn {
  97. background: #666;
  98. color: white;
  99. }
  100. .yt-dl-config-overlay {
  101. position: fixed;
  102. top: 0;
  103. left: 0;
  104. right: 0;
  105. bottom: 0;
  106. background: rgba(0,0,0,0.7);
  107. z-index: 9999;
  108. }
  109. `;
  110. document.head.appendChild(style);
  111. }
  112.  
  113. // Create dialog
  114. const overlay = document.createElement('div');
  115. overlay.className = 'yt-dl-config-overlay';
  116. const dialog = document.createElement('div');
  117. dialog.className = 'yt-dl-config-dialog';
  118. dialog.innerHTML = `
  119. <h2>⚙️ Configure YouTube Downloader</h2>
  120. <div class="input-group">
  121. <label for="api-key">API Key:</label>
  122. <input type="password" id="api-key" value="${API_KEY}" placeholder="Enter your API key">
  123. <label for="api-base">API Base URL:</label>
  124. <input type="text" id="api-base" value="${API_BASE}" placeholder="Enter your API base URL">
  125. </div>
  126. <div class="buttons">
  127. <button class="cancel-btn">Cancel</button>
  128. <button class="save-btn">Save</button>
  129. </div>
  130. `;
  131.  
  132. overlay.appendChild(dialog);
  133. document.body.appendChild(overlay);
  134.  
  135. // Handle buttons
  136. dialog.querySelector('.save-btn').addEventListener('click', () => {
  137. const newApiKey = dialog.querySelector('#api-key').value.trim();
  138. const newApiBase = dialog.querySelector('#api-base').value.trim();
  139.  
  140. if (newApiKey && newApiBase) {
  141. GM_setValue('API_KEY', newApiKey);
  142. GM_setValue('API_BASE', newApiBase);
  143. API_KEY = newApiKey;
  144. API_BASE = newApiBase;
  145. showNotification('Configuration saved successfully! ✅');
  146. document.body.removeChild(overlay);
  147. } else {
  148. showNotification('Please fill in all fields! ⚠️');
  149. }
  150. });
  151.  
  152. dialog.querySelector('.cancel-btn').addEventListener('click', () => {
  153. document.body.removeChild(overlay);
  154. });
  155. }
  156.  
  157. // Notification helper
  158. function showNotification(text, timeout = 3000) {
  159. GM_notification({
  160. text: text,
  161. title: 'YouTube Downloader',
  162. timeout: timeout
  163. });
  164. }
  165.  
  166. // Configuration check
  167. function checkConfig() {
  168. if (API_KEY === 'Your API Key' || API_BASE === 'Your API Base URL') {
  169. showNotification('Please configure the downloader first! Click the Tampermonkey icon and select "⚙️ Configure"');
  170. setTimeout(showConfig, 1000);
  171. return false;
  172. }
  173. return true;
  174. }
  175.  
  176. // Create and show download dialog
  177. function showDialog() {
  178. if (!checkConfig()) return;
  179.  
  180. // Create dialog styles
  181. const style = document.createElement('style');
  182. style.textContent = `
  183. .yt-dl-dialog {
  184. position: fixed;
  185. top: 50%;
  186. left: 50%;
  187. transform: translate(-50%, -50%);
  188. background: #1a1a1a;
  189. color: white;
  190. padding: 20px;
  191. border-radius: 8px;
  192. z-index: 10000;
  193. min-width: 300px;
  194. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  195. border: 1px solid #333;
  196. }
  197. .yt-dl-dialog h2 {
  198. margin: 0 0 15px 0;
  199. font-size: 18px;
  200. display: flex;
  201. align-items: center;
  202. gap: 8px;
  203. }
  204. .yt-dl-dialog select {
  205. width: 100%;
  206. padding: 8px;
  207. margin-bottom: 15px;
  208. background: #333;
  209. color: white;
  210. border: 1px solid #444;
  211. border-radius: 4px;
  212. }
  213. .yt-dl-dialog .buttons {
  214. display: flex;
  215. justify-content: space-between;
  216. gap: 10px;
  217. }
  218. .yt-dl-dialog button {
  219. padding: 8px 16px;
  220. border: none;
  221. border-radius: 4px;
  222. cursor: pointer;
  223. flex: 1;
  224. display: flex;
  225. align-items: center;
  226. justify-content: center;
  227. gap: 5px;
  228. }
  229. .yt-dl-dialog .download-btn {
  230. background: #2196F3;
  231. color: white;
  232. }
  233. .yt-dl-dialog .link-btn {
  234. background: #4CAF50;
  235. color: white;
  236. }
  237. .yt-dl-dialog .cancel-btn {
  238. background: #666;
  239. color: white;
  240. }
  241. .yt-dl-overlay {
  242. position: fixed;
  243. top: 0;
  244. left: 0;
  245. right: 0;
  246. bottom: 0;
  247. background: rgba(0,0,0,0.7);
  248. z-index: 9999;
  249. }
  250. #download-progress {
  251. position: fixed;
  252. bottom: 20px;
  253. right: 20px;
  254. background: #1a1a1a;
  255. color: white;
  256. padding: 15px;
  257. border-radius: 8px;
  258. z-index: 10001;
  259. min-width: 300px;
  260. box-shadow: 0 4px 12px rgba(0,0,0,0.15);
  261. display: none;
  262. }
  263. #download-progress .progress-bar {
  264. height: 5px;
  265. background: #333;
  266. border-radius: 3px;
  267. margin: 10px 0;
  268. overflow: hidden;
  269. }
  270. #download-progress .progress-bar-fill {
  271. height: 100%;
  272. background: #2196F3;
  273. width: 0%;
  274. transition: width 0.3s ease;
  275. }
  276. `;
  277. document.head.appendChild(style);
  278.  
  279. // Create dialog
  280. const overlay = document.createElement('div');
  281. overlay.className = 'yt-dl-overlay';
  282.  
  283. const dialog = document.createElement('div');
  284. dialog.className = 'yt-dl-dialog';
  285. dialog.innerHTML = `
  286. <h2>📥 Download Video</h2>
  287. <select id="quality-select">
  288. ${QUALITIES.map(q => `<option value="${q.value}">${q.label}</option>`).join('')}
  289. </select>
  290. <div class="buttons">
  291. <button class="download-btn">💾 Download</button>
  292. <button class="link-btn">🔗 Get Link</button>
  293. <button class="cancel-btn">❌ Cancel</button>
  294. </div>
  295. `;
  296.  
  297. overlay.appendChild(dialog);
  298. document.body.appendChild(overlay);
  299.  
  300. // Handle buttons
  301. dialog.querySelector('.download-btn').addEventListener('click', () => {
  302. const quality = dialog.querySelector('#quality-select').value;
  303. document.body.removeChild(overlay);
  304. streamDownload(quality);
  305. });
  306.  
  307. dialog.querySelector('.link-btn').addEventListener('click', () => {
  308. const quality = dialog.querySelector('#quality-select').value;
  309. document.body.removeChild(overlay);
  310. quickLink(quality);
  311. });
  312.  
  313. dialog.querySelector('.cancel-btn').addEventListener('click', () => {
  314. document.body.removeChild(overlay);
  315. });
  316. }
  317.  
  318. // Progress UI functions
  319. function showProgress(text, progress = null) {
  320. let container = document.getElementById('download-progress');
  321. if (!container) {
  322. container = document.createElement('div');
  323. container.id = 'download-progress';
  324. container.innerHTML = `
  325. <div class="status"></div>
  326. <div class="progress-bar">
  327. <div class="progress-bar-fill"></div>
  328. </div>
  329. `;
  330. document.body.appendChild(container);
  331. }
  332. container.style.display = 'block';
  333. container.querySelector('.status').textContent = text;
  334. if (progress !== null) {
  335. container.querySelector('.progress-bar-fill').style.width = `${progress}%`;
  336. }
  337. }
  338.  
  339. function hideProgress() {
  340. const container = document.getElementById('download-progress');
  341. if (container) {
  342. container.style.display = 'none';
  343. }
  344. }
  345.  
  346. // Download functions
  347. async function streamDownload(quality) {
  348. if (!checkConfig()) return;
  349.  
  350. const videoId = new URLSearchParams(window.location.search).get('v');
  351. if (!videoId) {
  352. showNotification('No video found! ❌');
  353. return;
  354. }
  355.  
  356. try {
  357. showProgress('Starting download...', 0);
  358. const url = `${API_BASE}/api/stream/${videoId}?quality=${quality}`;
  359. const title = document.title.replace(' - YouTube', '').trim();
  360. const ext = quality === 'audio' ? 'm4a' : 'mp4';
  361.  
  362. const response = await new Promise((resolve, reject) => {
  363. GM_xmlhttpRequest({
  364. method: 'GET',
  365. url,
  366. responseType: 'blob',
  367. headers: { 'X-API-Key': API_KEY },
  368. onprogress: (progress) => {
  369. if (progress.lengthComputable) {
  370. const percent = (progress.loaded / progress.total * 100).toFixed(1);
  371. showProgress(`Downloading: ${percent}%`, percent);
  372. }
  373. },
  374. onload: resolve,
  375. onerror: reject
  376. });
  377. });
  378.  
  379. const blob = new Blob([response.response]);
  380. const downloadUrl = URL.createObjectURL(blob);
  381. const a = document.createElement('a');
  382. a.href = downloadUrl;
  383. a.download = `${title}.${ext}`;
  384. document.body.appendChild(a);
  385. a.click();
  386. document.body.removeChild(a);
  387. URL.revokeObjectURL(downloadUrl);
  388.  
  389. showProgress('Download complete! ✅', 100);
  390. setTimeout(hideProgress, 3000);
  391. } catch (error) {
  392. console.error('Download error:', error);
  393. showProgress('Download failed! ❌');
  394. showNotification('Download failed! Check console for details.');
  395. setTimeout(hideProgress, 3000);
  396. }
  397. }
  398.  
  399. async function quickLink(quality) {
  400. if (!checkConfig()) return;
  401.  
  402. const videoId = new URLSearchParams(window.location.search).get('v');
  403. if (!videoId) {
  404. showNotification('No video found! ❌');
  405. return;
  406. }
  407.  
  408. try {
  409. showProgress('Getting download link...');
  410. const response = await new Promise((resolve, reject) => {
  411. GM_xmlhttpRequest({
  412. method: 'GET',
  413. url: `${API_BASE}/api/download/${videoId}?quality=${quality}`,
  414. headers: { 'X-API-Key': API_KEY },
  415. onload: (response) => {
  416. if (response.status === 200) {
  417. resolve(JSON.parse(response.responseText));
  418. } else {
  419. reject(new Error(response.statusText));
  420. }
  421. },
  422. onerror: reject
  423. });
  424. });
  425.  
  426. window.open(response.download_url, '_blank');
  427. showProgress('Link opened in new tab! ✅');
  428. setTimeout(hideProgress, 2000);
  429. } catch (error) {
  430. console.error('Error:', error);
  431. showProgress('Failed to get link! ❌');
  432. showNotification('Failed to get download link! Check console for details.');
  433. setTimeout(hideProgress, 3000);
  434. }
  435. }
  436.  
  437. // Add context menu button to video thumbnails
  438. function addContextMenuToThumbnails() {
  439. const thumbnails = document.querySelectorAll('a#thumbnail');
  440. thumbnails.forEach(thumbnail => {
  441. if (!thumbnail.dataset.dlEnabled) {
  442. thumbnail.addEventListener('contextmenu', (e) => {
  443. const videoId = thumbnail.href?.match(/[?&]v=([^&]+)/)?.[1];
  444. if (videoId) {
  445. e.preventDefault();
  446. const rect = thumbnail.getBoundingClientRect();
  447. showContextMenu(videoId, rect.left, rect.top);
  448. }
  449. });
  450. thumbnail.dataset.dlEnabled = 'true';
  451. }
  452. });
  453. }
  454.  
  455. // Context menu for thumbnails
  456. function showContextMenu(videoId, x, y) {
  457. const menu = document.createElement('div');
  458. menu.className = 'yt-dl-context-menu';
  459. menu.style.cssText = `
  460. position: fixed;
  461. left: ${x}px;
  462. top: ${y}px;
  463. background: #1a1a1a;
  464. border: 1px solid #333;
  465. border-radius: 4px;
  466. padding: 5px 0;
  467. z-index: 10000;
  468. box-shadow: 0 2px 10px rgba(0,0,0,0.2);
  469. `;
  470.  
  471. menu.innerHTML = `
  472. <div style="padding: 8px 12px; color: #fff; font-size: 14px; cursor: pointer; hover: background-color: #333;">
  473. 📥 Download Video
  474. </div>
  475. `;
  476.  
  477. document.body.appendChild(menu);
  478.  
  479. // Handle click
  480. menu.addEventListener('click', () => {
  481. window.location.href = `https://www.youtube.com/watch?v=${videoId}`;
  482. setTimeout(showDialog, 1000);
  483. });
  484.  
  485. // Remove menu on click outside
  486. function removeMenu(e) {
  487. if (!menu.contains(e.target)) {
  488. document.body.removeChild(menu);
  489. document.removeEventListener('click', removeMenu);
  490. }
  491. }
  492. setTimeout(() => document.addEventListener('click', removeMenu), 0);
  493. }
  494.  
  495. // YouTube spa navigation handler
  496. function handleSpaNavigation() {
  497. const observer = new MutationObserver((mutations) => {
  498. mutations.forEach((mutation) => {
  499. if (mutation.type === 'childList') {
  500. addContextMenuToThumbnails();
  501. }
  502. });
  503. });
  504.  
  505. observer.observe(document.body, {
  506. childList: true,
  507. subtree: true
  508. });
  509. }
  510.  
  511. // Initialize
  512. function init() {
  513. // Add initial context menus
  514. addContextMenuToThumbnails();
  515. // Handle SPA navigation
  516. handleSpaNavigation();
  517. // Add configuration command
  518. GM_registerMenuCommand('⚙️ Configure', showConfig);
  519. // Add download command
  520. GM_registerMenuCommand('📥 Download Video', showDialog);
  521. }
  522.  
  523. // Start the script
  524. init();
  525. })();