enhanced-download-methods.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /**
  2. * @fileoverview Enhanced download methods with real yt-dlp integration
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. /**
  8. * ENHANCED DOWNLOAD METHODS
  9. *
  10. * Real video download functionality with yt-dlp integration
  11. * Replaces placeholder methods with actual IPC communication
  12. *
  13. * Features:
  14. * - Real video downloads with progress tracking
  15. * - Status transitions (Ready → Downloading → Converting → Completed)
  16. * - Enhanced metadata extraction with thumbnails
  17. * - Error handling with user-friendly messages
  18. *
  19. * Dependencies:
  20. * - Electron IPC (window.electronAPI)
  21. * - Main process download handlers
  22. * - URLValidator utility class
  23. */
  24. /**
  25. * Enhanced video download with full IPC integration and progress tracking
  26. * Replaces placeholder handleDownloadVideos method
  27. */
  28. async function handleDownloadVideos() {
  29. const readyVideos = this.state.getVideosByStatus('ready');
  30. if (readyVideos.length === 0) {
  31. this.showStatus('No videos ready for download', 'info');
  32. return;
  33. }
  34. if (!window.electronAPI) {
  35. this.showStatus('Video download not available in browser mode', 'error');
  36. return;
  37. }
  38. // Check if save path is configured
  39. if (!this.state.config.savePath) {
  40. this.showStatus('Please select a save directory first', 'error');
  41. return;
  42. }
  43. try {
  44. // Set downloading state
  45. this.state.updateUI({ isDownloading: true });
  46. this.updateControlPanelState();
  47. // Set up progress listener for this download session
  48. const progressListenerId = 'download-session-' + Date.now();
  49. const progressCleanup = window.electronAPI.onDownloadProgress((event, progressData) => {
  50. this.handleDownloadProgress(progressData);
  51. });
  52. this.showStatus(`Starting download of ${readyVideos.length} video(s)...`, 'info');
  53. console.log('Starting downloads for videos:', readyVideos.map(v => ({ id: v.id, url: v.url, title: v.title })));
  54. let completedCount = 0;
  55. let errorCount = 0;
  56. // PARALLEL DOWNLOADS: Start all downloads simultaneously
  57. // The DownloadManager on the backend will handle concurrency limits
  58. const downloadPromises = readyVideos.map(async (video) => {
  59. try {
  60. console.log(`Queueing download for video ${video.id}: ${video.title}`);
  61. // Update video status to downloading (queued)
  62. this.state.updateVideo(video.id, {
  63. status: 'downloading',
  64. progress: 0,
  65. error: null
  66. });
  67. this.renderVideoList();
  68. // Prepare download options
  69. const downloadOptions = {
  70. videoId: video.id, // IMPORTANT: Pass videoId for queue management
  71. url: video.url,
  72. quality: video.quality,
  73. format: video.format,
  74. savePath: this.state.config.savePath,
  75. cookieFile: this.state.config.cookieFile
  76. };
  77. console.log(`Download options for video ${video.id}:`, downloadOptions);
  78. // Start download (returns immediately, queued in DownloadManager)
  79. const result = await window.electronAPI.downloadVideo(downloadOptions);
  80. if (result.success) {
  81. // Update video status to completed
  82. this.state.updateVideo(video.id, {
  83. status: 'completed',
  84. progress: 100,
  85. filename: result.filename || 'Downloaded',
  86. error: null
  87. });
  88. completedCount++;
  89. console.log(`Successfully downloaded video ${video.id}: ${video.title}`);
  90. } else {
  91. throw new Error(result.error || 'Download failed');
  92. }
  93. } catch (error) {
  94. console.error(`Failed to download video ${video.id}:`, error);
  95. // Update video status to error
  96. this.state.updateVideo(video.id, {
  97. status: 'error',
  98. error: error.message,
  99. progress: 0
  100. });
  101. errorCount++;
  102. return { success: false, videoId: video.id, error: error.message };
  103. }
  104. });
  105. // Wait for ALL downloads to complete in parallel
  106. console.log(`⚡ Starting ${downloadPromises.length} parallel downloads (DownloadManager handles concurrency)...`);
  107. await Promise.allSettled(downloadPromises);
  108. // Update UI after all downloads complete
  109. this.renderVideoList();
  110. // Clean up progress listener
  111. if (progressCleanup) {
  112. progressCleanup();
  113. }
  114. // Update final state
  115. this.state.updateUI({ isDownloading: false });
  116. this.updateControlPanelState();
  117. // Show final status
  118. if (errorCount === 0) {
  119. this.showStatus(`✅ Successfully downloaded ${completedCount} video(s) in parallel`, 'success');
  120. } else if (completedCount === 0) {
  121. this.showStatus(`❌ All ${errorCount} download(s) failed`, 'error');
  122. } else {
  123. this.showStatus(`⚠️ Downloaded ${completedCount} video(s), ${errorCount} failed`, 'warning');
  124. }
  125. console.log(`🏁 Download session completed: ${completedCount} successful, ${errorCount} failed`);
  126. } catch (error) {
  127. console.error('Error in download process:', error);
  128. this.showStatus(`Download process failed: ${error.message}`, 'error');
  129. // Reset state on error
  130. this.state.updateUI({ isDownloading: false });
  131. this.updateControlPanelState();
  132. }
  133. }
  134. /**
  135. * Enhanced metadata fetching with real yt-dlp integration
  136. * Replaces placeholder fetchVideoMetadata method
  137. */
  138. async function fetchVideoMetadata(videoId, url) {
  139. try {
  140. console.log(`Starting metadata fetch for video ${videoId}:`, url);
  141. // Update video status to indicate metadata loading
  142. this.state.updateVideo(videoId, {
  143. title: 'Loading metadata...',
  144. status: 'ready'
  145. });
  146. this.renderVideoList();
  147. // Extract thumbnail immediately (this is fast for YouTube)
  148. const thumbnail = await URLValidator.extractThumbnail(url);
  149. // Update video with thumbnail first if available
  150. if (thumbnail) {
  151. this.state.updateVideo(videoId, { thumbnail });
  152. this.renderVideoList();
  153. }
  154. // Fetch real metadata using Electron IPC if available
  155. let metadata;
  156. if (window.electronAPI) {
  157. try {
  158. console.log(`Fetching real metadata for video ${videoId} via IPC`);
  159. metadata = await window.electronAPI.getVideoMetadata(url);
  160. console.log(`Real metadata received for video ${videoId}:`, metadata);
  161. } catch (error) {
  162. console.warn(`Failed to fetch real metadata for video ${videoId}, using fallback:`, error);
  163. metadata = await this.simulateMetadataFetch(url);
  164. }
  165. } else {
  166. // Fallback to simulation in browser mode
  167. console.warn('Electron API not available, using simulation for metadata');
  168. metadata = await this.simulateMetadataFetch(url);
  169. }
  170. // Update video with fetched metadata
  171. if (metadata) {
  172. const updateData = {
  173. title: metadata.title || 'Unknown Title',
  174. duration: metadata.duration || '00:00',
  175. status: 'ready',
  176. error: null
  177. };
  178. // Use fetched thumbnail if available, otherwise keep the one we extracted
  179. if (metadata.thumbnail && (!thumbnail || metadata.thumbnail !== thumbnail)) {
  180. updateData.thumbnail = metadata.thumbnail;
  181. }
  182. this.state.updateVideo(videoId, updateData);
  183. this.renderVideoList();
  184. console.log(`Metadata successfully updated for video ${videoId}:`, updateData);
  185. }
  186. } catch (error) {
  187. console.error(`Failed to fetch metadata for video ${videoId}:`, error);
  188. // Update video with error state but keep it downloadable
  189. this.state.updateVideo(videoId, {
  190. title: 'Metadata unavailable',
  191. status: 'ready',
  192. error: null // Clear any previous errors since this is just metadata
  193. });
  194. this.renderVideoList();
  195. }
  196. }
  197. /**
  198. * Handle download progress updates from IPC with enhanced status transitions
  199. */
  200. function handleDownloadProgress(progressData) {
  201. const { url, progress, status, stage, conversionSpeed } = progressData;
  202. console.log('Download progress update:', progressData);
  203. // Find video by URL and update progress
  204. const video = this.state.videos.find(v => v.url === url);
  205. if (video) {
  206. const updateData = { progress };
  207. // Update status based on stage with enhanced conversion handling
  208. if (stage === 'download' && status === 'downloading') {
  209. updateData.status = 'downloading';
  210. } else if ((stage === 'postprocess' || stage === 'conversion') && status === 'converting') {
  211. updateData.status = 'converting';
  212. // Add conversion speed info if available
  213. if (conversionSpeed) {
  214. updateData.conversionSpeed = conversionSpeed;
  215. }
  216. } else if (stage === 'complete' && status === 'completed') {
  217. updateData.status = 'completed';
  218. updateData.progress = 100;
  219. }
  220. this.state.updateVideo(video.id, updateData);
  221. this.renderVideoList();
  222. const speedInfo = conversionSpeed ? ` (${conversionSpeed}x speed)` : '';
  223. console.log(`Progress updated for video ${video.id}: ${progress}% (${status})${speedInfo}`);
  224. } else {
  225. console.warn('Received progress update for unknown video URL:', url);
  226. }
  227. }
  228. /**
  229. * Enhanced binary checking with detailed status reporting
  230. */
  231. async function checkBinaries() {
  232. if (!window.electronAPI) {
  233. console.warn('Electron API not available - running in browser mode');
  234. this.showStatus('Running in browser mode - download functionality limited', 'warning');
  235. return;
  236. }
  237. try {
  238. console.log('Checking yt-dlp and ffmpeg binaries...');
  239. this.showStatus('Checking dependencies...', 'info');
  240. const binaryVersions = await window.electronAPI.checkBinaryVersions();
  241. // Update UI based on binary availability
  242. this.updateBinaryStatus(binaryVersions);
  243. if (binaryVersions.ytDlp.available && binaryVersions.ffmpeg.available) {
  244. console.log('All required binaries are available');
  245. console.log('yt-dlp version:', binaryVersions.ytDlp.version);
  246. console.log('ffmpeg version:', binaryVersions.ffmpeg.version);
  247. this.showStatus('All dependencies ready', 'success');
  248. } else {
  249. const missing = [];
  250. if (!binaryVersions.ytDlp.available) missing.push('yt-dlp');
  251. if (!binaryVersions.ffmpeg.available) missing.push('ffmpeg');
  252. console.warn('Missing binaries:', missing);
  253. this.showStatus(`Missing dependencies: ${missing.join(', ')}`, 'error');
  254. }
  255. // Store binary status for reference
  256. this.state.binaryStatus = binaryVersions;
  257. } catch (error) {
  258. console.error('Error checking binaries:', error);
  259. this.showStatus('Failed to check dependencies', 'error');
  260. }
  261. }
  262. /**
  263. * Update binary status UI based on version check results
  264. */
  265. function updateBinaryStatus(binaryVersions) {
  266. console.log('Binary status updated:', binaryVersions);
  267. // Update dependency status indicators if they exist
  268. const ytDlpStatus = document.getElementById('ytdlp-status');
  269. if (ytDlpStatus) {
  270. ytDlpStatus.textContent = binaryVersions.ytDlp.available
  271. ? `yt-dlp ${binaryVersions.ytDlp.version}`
  272. : 'yt-dlp missing';
  273. ytDlpStatus.className = binaryVersions.ytDlp.available ? 'status-ok' : 'status-error';
  274. }
  275. const ffmpegStatus = document.getElementById('ffmpeg-status');
  276. if (ffmpegStatus) {
  277. ffmpegStatus.textContent = binaryVersions.ffmpeg.available
  278. ? `ffmpeg ${binaryVersions.ffmpeg.version}`
  279. : 'ffmpeg missing';
  280. ffmpegStatus.className = binaryVersions.ffmpeg.available ? 'status-ok' : 'status-error';
  281. }
  282. // Update update dependencies button if updates are available
  283. const updateBtn = document.getElementById('updateDependenciesBtn');
  284. if (updateBtn) {
  285. // This would be enhanced in future tasks to show actual update availability
  286. updateBtn.disabled = false;
  287. }
  288. }
  289. /**
  290. * Enhanced file selection handlers
  291. */
  292. async function handleSelectSaveDirectory() {
  293. if (!window.electronAPI) {
  294. this.showStatus('Directory selection not available in browser mode', 'error');
  295. return;
  296. }
  297. try {
  298. this.showStatus('Opening directory dialog...', 'info');
  299. const directoryPath = await window.electronAPI.selectSaveDirectory();
  300. if (directoryPath) {
  301. // Update configuration with selected directory
  302. this.state.updateConfig({ savePath: directoryPath });
  303. // Update UI to show selected directory
  304. this.updateSavePathUI(directoryPath);
  305. this.showStatus('Save directory selected successfully', 'success');
  306. console.log('Save directory selected:', directoryPath);
  307. } else {
  308. this.showStatus('Directory selection cancelled', 'info');
  309. }
  310. } catch (error) {
  311. console.error('Error selecting save directory:', error);
  312. this.showStatus('Failed to select save directory', 'error');
  313. }
  314. }
  315. async function handleSelectCookieFile() {
  316. if (!window.electronAPI) {
  317. this.showStatus('File selection not available in browser mode', 'error');
  318. return;
  319. }
  320. try {
  321. this.showStatus('Opening file dialog...', 'info');
  322. const cookieFilePath = await window.electronAPI.selectCookieFile();
  323. if (cookieFilePath) {
  324. // Update configuration with selected cookie file
  325. this.state.updateConfig({ cookieFile: cookieFilePath });
  326. // Update UI to show selected file
  327. this.updateCookieFileUI(cookieFilePath);
  328. this.showStatus('Cookie file selected successfully', 'success');
  329. console.log('Cookie file selected:', cookieFilePath);
  330. } else {
  331. this.showStatus('Cookie file selection cancelled', 'info');
  332. }
  333. } catch (error) {
  334. console.error('Error selecting cookie file:', error);
  335. this.showStatus('Failed to select cookie file', 'error');
  336. }
  337. }
  338. /**
  339. * UI update helpers
  340. */
  341. function updateSavePathUI(directoryPath) {
  342. const savePath = document.getElementById('savePath');
  343. if (savePath) {
  344. savePath.textContent = directoryPath;
  345. savePath.title = directoryPath;
  346. }
  347. const savePathBtn = document.getElementById('savePathBtn');
  348. if (savePathBtn) {
  349. savePathBtn.classList.add('selected');
  350. }
  351. }
  352. function updateCookieFileUI(cookieFilePath) {
  353. const cookieFileBtn = document.getElementById('cookieFileBtn');
  354. if (cookieFileBtn) {
  355. // Update button text to show file is selected
  356. const fileName = cookieFilePath.split('/').pop() || cookieFilePath.split('\\').pop();
  357. cookieFileBtn.textContent = `Cookie File: ${fileName}`;
  358. cookieFileBtn.title = cookieFilePath;
  359. cookieFileBtn.classList.add('selected');
  360. }
  361. }
  362. /**
  363. * Cancel active conversions for specific video or all videos
  364. */
  365. async function handleCancelConversions(videoId = null) {
  366. if (!window.electronAPI) {
  367. this.showStatus('Conversion cancellation not available in browser mode', 'error');
  368. return;
  369. }
  370. try {
  371. let result;
  372. if (videoId) {
  373. // Cancel conversion for specific video (would need conversion ID tracking)
  374. this.showStatus('Cancelling conversion...', 'info');
  375. result = await window.electronAPI.cancelAllConversions(); // Simplified for now
  376. } else {
  377. // Cancel all active conversions
  378. this.showStatus('Cancelling all conversions...', 'info');
  379. result = await window.electronAPI.cancelAllConversions();
  380. }
  381. if (result.success) {
  382. // Update video statuses for cancelled conversions
  383. const convertingVideos = this.state.getVideosByStatus('converting');
  384. convertingVideos.forEach(video => {
  385. this.state.updateVideo(video.id, {
  386. status: 'ready',
  387. progress: 0,
  388. error: 'Conversion cancelled by user'
  389. });
  390. });
  391. this.renderVideoList();
  392. this.showStatus(result.message || 'Conversions cancelled successfully', 'success');
  393. console.log('Conversions cancelled:', result);
  394. } else {
  395. this.showStatus('Failed to cancel conversions', 'error');
  396. }
  397. } catch (error) {
  398. console.error('Error cancelling conversions:', error);
  399. this.showStatus(`Failed to cancel conversions: ${error.message}`, 'error');
  400. }
  401. }
  402. /**
  403. * Get information about active conversions
  404. */
  405. async function getActiveConversions() {
  406. if (!window.electronAPI) {
  407. return { success: false, conversions: [] };
  408. }
  409. try {
  410. const result = await window.electronAPI.getActiveConversions();
  411. return result;
  412. } catch (error) {
  413. console.error('Error getting active conversions:', error);
  414. return { success: false, conversions: [], error: error.message };
  415. }
  416. }
  417. /**
  418. * Enhanced cancel downloads to include conversion cancellation
  419. */
  420. async function handleCancelDownloads() {
  421. if (!window.electronAPI) {
  422. this.showStatus('Download cancellation not available in browser mode', 'error');
  423. return;
  424. }
  425. try {
  426. this.showStatus('Cancelling downloads and conversions...', 'info');
  427. // Cancel any active conversions first
  428. await this.handleCancelConversions();
  429. // Update all processing videos to ready state
  430. const processingVideos = this.state.videos.filter(v =>
  431. ['downloading', 'converting'].includes(v.status)
  432. );
  433. processingVideos.forEach(video => {
  434. this.state.updateVideo(video.id, {
  435. status: 'ready',
  436. progress: 0,
  437. error: null
  438. });
  439. });
  440. this.state.updateUI({ isDownloading: false });
  441. this.updateControlPanelState();
  442. this.renderVideoList();
  443. this.showStatus(`Cancelled ${processingVideos.length} active operations`, 'success');
  444. console.log(`Cancelled ${processingVideos.length} downloads/conversions`);
  445. } catch (error) {
  446. console.error('Error cancelling downloads:', error);
  447. this.showStatus(`Failed to cancel operations: ${error.message}`, 'error');
  448. }
  449. }
  450. // Export methods for integration into main app
  451. if (typeof module !== 'undefined' && module.exports) {
  452. module.exports = {
  453. handleDownloadVideos,
  454. fetchVideoMetadata,
  455. handleDownloadProgress,
  456. checkBinaries,
  457. updateBinaryStatus,
  458. handleSelectSaveDirectory,
  459. handleSelectCookieFile,
  460. updateSavePathUI,
  461. updateCookieFileUI,
  462. handleCancelConversions,
  463. getActiveConversions,
  464. handleCancelDownloads
  465. };
  466. } else if (typeof window !== 'undefined') {
  467. // Make methods available globally for integration
  468. window.EnhancedDownloadMethods = {
  469. handleDownloadVideos,
  470. fetchVideoMetadata,
  471. handleDownloadProgress,
  472. checkBinaries,
  473. updateBinaryStatus,
  474. handleSelectSaveDirectory,
  475. handleSelectCookieFile,
  476. updateSavePathUI,
  477. updateCookieFileUI,
  478. handleCancelConversions,
  479. getActiveConversions,
  480. handleCancelDownloads
  481. };
  482. }