error-handler.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446
  1. /**
  2. * @fileoverview Enhanced error handling utilities for desktop application
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. * @since 2024-01-01
  6. */
  7. /**
  8. * ERROR HANDLING UTILITIES
  9. *
  10. * Provides comprehensive error handling for the desktop application
  11. * including user-friendly error messages, desktop notifications,
  12. * and error recovery suggestions.
  13. */
  14. /**
  15. * Error types and their corresponding user-friendly messages
  16. */
  17. const ERROR_TYPES = {
  18. NETWORK: {
  19. type: 'network',
  20. title: 'Network Error',
  21. icon: 'assets/icons/close.svg',
  22. color: '#e7000b',
  23. recoverable: true
  24. },
  25. BINARY_MISSING: {
  26. type: 'binary_missing',
  27. title: 'Missing Dependencies',
  28. icon: 'assets/icons/close.svg',
  29. color: '#e7000b',
  30. recoverable: false
  31. },
  32. PERMISSION: {
  33. type: 'permission',
  34. title: 'Permission Error',
  35. icon: 'assets/icons/close.svg',
  36. color: '#e7000b',
  37. recoverable: true
  38. },
  39. VIDEO_UNAVAILABLE: {
  40. type: 'video_unavailable',
  41. title: 'Video Unavailable',
  42. icon: 'assets/icons/close.svg',
  43. color: '#e7000b',
  44. recoverable: false
  45. },
  46. AGE_RESTRICTED: {
  47. type: 'age_restricted',
  48. title: 'Age Restricted Content',
  49. icon: 'assets/icons/close.svg',
  50. color: '#e7000b',
  51. recoverable: true
  52. },
  53. DISK_SPACE: {
  54. type: 'disk_space',
  55. title: 'Insufficient Storage',
  56. icon: 'assets/icons/close.svg',
  57. color: '#e7000b',
  58. recoverable: true
  59. },
  60. FORMAT_ERROR: {
  61. type: 'format_error',
  62. title: 'Format Error',
  63. icon: 'assets/icons/close.svg',
  64. color: '#e7000b',
  65. recoverable: true
  66. },
  67. UNKNOWN: {
  68. type: 'unknown',
  69. title: 'Unknown Error',
  70. icon: 'assets/icons/close.svg',
  71. color: '#e7000b',
  72. recoverable: false
  73. }
  74. };
  75. /**
  76. * Enhanced Error Handler Class
  77. *
  78. * Provides centralized error handling with desktop notifications,
  79. * user-friendly messages, and recovery suggestions.
  80. */
  81. class ErrorHandler {
  82. constructor() {
  83. this.errorHistory = [];
  84. this.maxHistorySize = 50;
  85. this.notificationQueue = [];
  86. this.isProcessingNotifications = false;
  87. }
  88. /**
  89. * Handle and display error with appropriate user feedback
  90. * @param {Error|string} error - Error object or message
  91. * @param {Object} context - Additional context about the error
  92. * @param {Object} options - Display options
  93. */
  94. async handleError(error, context = {}, options = {}) {
  95. const errorInfo = this.parseError(error, context);
  96. // Add to error history
  97. this.addToHistory(errorInfo);
  98. // Show appropriate user feedback
  99. await this.showErrorFeedback(errorInfo, options);
  100. // Log error for debugging
  101. this.logError(errorInfo);
  102. return errorInfo;
  103. }
  104. /**
  105. * Parse error and extract meaningful information
  106. * @param {Error|string} error - Error to parse
  107. * @param {Object} context - Additional context
  108. * @returns {Object} Parsed error information
  109. */
  110. parseError(error, context = {}) {
  111. const errorInfo = {
  112. id: this.generateErrorId(),
  113. timestamp: new Date(),
  114. originalError: error,
  115. context,
  116. type: ERROR_TYPES.UNKNOWN,
  117. message: 'An unknown error occurred',
  118. suggestion: 'Please try again or contact support',
  119. recoverable: false,
  120. technical: null
  121. };
  122. // Extract error message
  123. const errorMessage = typeof error === 'string' ? error : error?.message || 'Unknown error';
  124. const lowerMessage = errorMessage.toLowerCase();
  125. // Determine error type and provide appropriate messaging
  126. if (lowerMessage.includes('network') || lowerMessage.includes('connection') || lowerMessage.includes('timeout')) {
  127. errorInfo.type = ERROR_TYPES.NETWORK;
  128. errorInfo.message = 'Network connection error';
  129. errorInfo.suggestion = 'Check your internet connection and try again';
  130. errorInfo.recoverable = true;
  131. }
  132. else if (lowerMessage.includes('binary not found') || lowerMessage.includes('yt-dlp') || lowerMessage.includes('ffmpeg')) {
  133. errorInfo.type = ERROR_TYPES.BINARY_MISSING;
  134. errorInfo.message = 'Required application components are missing';
  135. errorInfo.suggestion = 'Please reinstall the application or check the setup';
  136. errorInfo.recoverable = false;
  137. }
  138. else if (lowerMessage.includes('permission') || lowerMessage.includes('access denied') || lowerMessage.includes('not writable')) {
  139. errorInfo.type = ERROR_TYPES.PERMISSION;
  140. errorInfo.message = 'Permission denied';
  141. errorInfo.suggestion = 'Check folder permissions or choose a different location';
  142. errorInfo.recoverable = true;
  143. }
  144. else if (lowerMessage.includes('unavailable') || lowerMessage.includes('private') || lowerMessage.includes('removed')) {
  145. errorInfo.type = ERROR_TYPES.VIDEO_UNAVAILABLE;
  146. errorInfo.message = 'Video is unavailable or has been removed';
  147. errorInfo.suggestion = 'Check if the video URL is correct and publicly accessible';
  148. errorInfo.recoverable = false;
  149. }
  150. else if (lowerMessage.includes('age') || lowerMessage.includes('restricted') || lowerMessage.includes('sign in')) {
  151. errorInfo.type = ERROR_TYPES.AGE_RESTRICTED;
  152. errorInfo.message = 'Age-restricted content requires authentication';
  153. errorInfo.suggestion = 'Use a cookie file from your browser to access this content';
  154. errorInfo.recoverable = true;
  155. }
  156. else if (lowerMessage.includes('space') || lowerMessage.includes('disk full') || lowerMessage.includes('no space')) {
  157. errorInfo.type = ERROR_TYPES.DISK_SPACE;
  158. errorInfo.message = 'Insufficient disk space';
  159. errorInfo.suggestion = 'Free up disk space or choose a different download location';
  160. errorInfo.recoverable = true;
  161. }
  162. else if (lowerMessage.includes('format') || lowerMessage.includes('quality') || lowerMessage.includes('resolution')) {
  163. errorInfo.type = ERROR_TYPES.FORMAT_ERROR;
  164. errorInfo.message = 'Requested video quality or format not available';
  165. errorInfo.suggestion = 'Try a different quality setting or use "Best Available"';
  166. errorInfo.recoverable = true;
  167. }
  168. else {
  169. // Use the original error message if it's reasonably short and descriptive
  170. if (errorMessage.length < 150 && errorMessage.length > 10) {
  171. errorInfo.message = errorMessage;
  172. }
  173. }
  174. // Store technical details
  175. errorInfo.technical = {
  176. message: errorMessage,
  177. stack: error?.stack,
  178. code: error?.code,
  179. context
  180. };
  181. return errorInfo;
  182. }
  183. /**
  184. * Show appropriate error feedback to user
  185. * @param {Object} errorInfo - Parsed error information
  186. * @param {Object} options - Display options
  187. */
  188. async showErrorFeedback(errorInfo, options = {}) {
  189. const {
  190. showNotification = true,
  191. showDialog = false,
  192. showInUI = true,
  193. priority = 'normal'
  194. } = options;
  195. // Show desktop notification
  196. if (showNotification && window.electronAPI) {
  197. await this.showErrorNotification(errorInfo, priority);
  198. }
  199. // Show error dialog for critical errors
  200. if (showDialog && window.electronAPI) {
  201. await this.showErrorDialog(errorInfo);
  202. }
  203. // Show in-app error message
  204. if (showInUI) {
  205. this.showInAppError(errorInfo);
  206. }
  207. }
  208. /**
  209. * Show desktop notification for error
  210. * @param {Object} errorInfo - Error information
  211. * @param {string} priority - Notification priority
  212. */
  213. async showErrorNotification(errorInfo, priority = 'normal') {
  214. try {
  215. const notificationOptions = {
  216. title: errorInfo.type.title,
  217. message: errorInfo.message,
  218. icon: errorInfo.type.icon,
  219. sound: priority === 'high',
  220. timeout: priority === 'high' ? 10 : 5
  221. };
  222. await window.electronAPI.showNotification(notificationOptions);
  223. } catch (error) {
  224. console.error('Failed to show error notification:', error);
  225. }
  226. }
  227. /**
  228. * Show error dialog for critical errors
  229. * @param {Object} errorInfo - Error information
  230. */
  231. async showErrorDialog(errorInfo) {
  232. try {
  233. const dialogOptions = {
  234. title: errorInfo.type.title,
  235. message: errorInfo.message,
  236. detail: errorInfo.suggestion,
  237. buttons: errorInfo.recoverable ? ['Retry', 'Cancel'] : ['OK'],
  238. defaultId: 0
  239. };
  240. const result = await window.electronAPI.showErrorDialog(dialogOptions);
  241. return result.response === 0; // Return true if user clicked retry/ok
  242. } catch (error) {
  243. console.error('Failed to show error dialog:', error);
  244. return false;
  245. }
  246. }
  247. /**
  248. * Show in-app error message
  249. * @param {Object} errorInfo - Error information
  250. */
  251. showInAppError(errorInfo) {
  252. // Dispatch custom event for UI components to handle
  253. const errorEvent = new CustomEvent('app-error', {
  254. detail: {
  255. id: errorInfo.id,
  256. type: errorInfo.type.type,
  257. message: errorInfo.message,
  258. suggestion: errorInfo.suggestion,
  259. recoverable: errorInfo.recoverable,
  260. timestamp: errorInfo.timestamp
  261. }
  262. });
  263. document.dispatchEvent(errorEvent);
  264. }
  265. /**
  266. * Handle binary dependency errors specifically
  267. * @param {string} binaryName - Name of missing binary
  268. * @param {Object} context - Additional context
  269. */
  270. async handleBinaryError(binaryName, context = {}) {
  271. const errorInfo = {
  272. id: this.generateErrorId(),
  273. timestamp: new Date(),
  274. type: ERROR_TYPES.BINARY_MISSING,
  275. message: `${binaryName} is required but not found`,
  276. suggestion: `Please ensure ${binaryName} is properly installed in the binaries directory`,
  277. recoverable: false,
  278. context: { binaryName, ...context }
  279. };
  280. await this.showErrorFeedback(errorInfo, {
  281. showNotification: true,
  282. showDialog: true,
  283. priority: 'high'
  284. });
  285. return errorInfo;
  286. }
  287. /**
  288. * Handle network-related errors with retry logic
  289. * @param {Error} error - Network error
  290. * @param {Function} retryCallback - Function to retry the operation
  291. * @param {number} maxRetries - Maximum retry attempts
  292. */
  293. async handleNetworkError(error, retryCallback = null, maxRetries = 3) {
  294. const errorInfo = this.parseError(error, { type: 'network', maxRetries });
  295. if (retryCallback && maxRetries > 0) {
  296. const shouldRetry = await this.showErrorDialog(errorInfo);
  297. if (shouldRetry) {
  298. try {
  299. return await retryCallback();
  300. } catch (retryError) {
  301. return this.handleNetworkError(retryError, retryCallback, maxRetries - 1);
  302. }
  303. }
  304. } else {
  305. await this.showErrorFeedback(errorInfo, { showNotification: true });
  306. }
  307. return errorInfo;
  308. }
  309. /**
  310. * Add error to history for debugging and analytics
  311. * @param {Object} errorInfo - Error information
  312. */
  313. addToHistory(errorInfo) {
  314. this.errorHistory.unshift(errorInfo);
  315. // Limit history size
  316. if (this.errorHistory.length > this.maxHistorySize) {
  317. this.errorHistory = this.errorHistory.slice(0, this.maxHistorySize);
  318. }
  319. }
  320. /**
  321. * Get error history for debugging
  322. * @returns {Array} Error history
  323. */
  324. getErrorHistory() {
  325. return [...this.errorHistory];
  326. }
  327. /**
  328. * Clear error history
  329. */
  330. clearHistory() {
  331. this.errorHistory = [];
  332. }
  333. /**
  334. * Log error for debugging
  335. * @param {Object} errorInfo - Error information
  336. */
  337. logError(errorInfo) {
  338. console.group(`🚨 Error [${errorInfo.type.type}] - ${errorInfo.id}`);
  339. console.error('Message:', errorInfo.message);
  340. console.error('Suggestion:', errorInfo.suggestion);
  341. console.error('Recoverable:', errorInfo.recoverable);
  342. console.error('Context:', errorInfo.context);
  343. if (errorInfo.technical) {
  344. console.error('Technical Details:', errorInfo.technical);
  345. }
  346. console.groupEnd();
  347. }
  348. /**
  349. * Generate unique error ID
  350. * @returns {string} Unique error ID
  351. */
  352. generateErrorId() {
  353. return `error_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  354. }
  355. /**
  356. * Check if error is recoverable
  357. * @param {Object} errorInfo - Error information
  358. * @returns {boolean} Whether error is recoverable
  359. */
  360. isRecoverable(errorInfo) {
  361. return errorInfo.recoverable === true;
  362. }
  363. /**
  364. * Get error statistics
  365. * @returns {Object} Error statistics
  366. */
  367. getStats() {
  368. const stats = {
  369. total: this.errorHistory.length,
  370. byType: {},
  371. recoverable: 0,
  372. recent: 0 // Last hour
  373. };
  374. const oneHourAgo = new Date(Date.now() - 60 * 60 * 1000);
  375. this.errorHistory.forEach(error => {
  376. // Count by type
  377. const type = error.type.type;
  378. stats.byType[type] = (stats.byType[type] || 0) + 1;
  379. // Count recoverable
  380. if (error.recoverable) {
  381. stats.recoverable++;
  382. }
  383. // Count recent
  384. if (error.timestamp > oneHourAgo) {
  385. stats.recent++;
  386. }
  387. });
  388. return stats;
  389. }
  390. }
  391. // Create global error handler instance
  392. const errorHandler = new ErrorHandler();
  393. // Export for use in other modules
  394. if (typeof module !== 'undefined' && module.exports) {
  395. module.exports = { ErrorHandler, errorHandler, ERROR_TYPES };
  396. } else {
  397. // Browser environment - attach to window
  398. window.ErrorHandler = ErrorHandler;
  399. window.errorHandler = errorHandler;
  400. window.ERROR_TYPES = ERROR_TYPES;
  401. }