enhanced-download-methods.js 20 KB

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