metadata-service.js 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. /**
  2. * @fileoverview Metadata Service for fetching video information via IPC
  3. * @author GrabZilla Development Team
  4. * @version 2.1.0
  5. */
  6. /**
  7. * METADATA SERVICE
  8. *
  9. * Fetches video metadata (title, thumbnail, duration) from URLs using yt-dlp
  10. * via the Electron IPC bridge.
  11. *
  12. * Features:
  13. * - Async metadata fetching with timeout
  14. * - Caching to avoid duplicate requests
  15. * - Error handling and fallback
  16. * - Support for YouTube and Vimeo
  17. */
  18. class MetadataService {
  19. constructor() {
  20. this.cache = new Map();
  21. this.pendingRequests = new Map();
  22. this.timeout = 30000; // 30 second timeout
  23. this.maxRetries = 2; // Maximum retry attempts
  24. this.retryDelay = 2000; // 2 second delay between retries
  25. this.ipcAvailable = typeof window !== 'undefined' && window.IPCManager;
  26. }
  27. /**
  28. * Get video metadata from URL
  29. * @param {string} url - Video URL to fetch metadata for
  30. * @returns {Promise<Object>} Video metadata object
  31. */
  32. async getVideoMetadata(url) {
  33. if (!url || typeof url !== 'string') {
  34. throw new Error('Valid URL is required');
  35. }
  36. const normalizedUrl = this.normalizeUrl(url);
  37. // Check cache first
  38. if (this.cache.has(normalizedUrl)) {
  39. return this.cache.get(normalizedUrl);
  40. }
  41. // Check if request is already pending
  42. if (this.pendingRequests.has(normalizedUrl)) {
  43. return this.pendingRequests.get(normalizedUrl);
  44. }
  45. // Create new request
  46. const requestPromise = this.fetchMetadata(normalizedUrl);
  47. this.pendingRequests.set(normalizedUrl, requestPromise);
  48. try {
  49. const metadata = await requestPromise;
  50. // Cache the result
  51. this.cache.set(normalizedUrl, metadata);
  52. return metadata;
  53. } finally {
  54. // Clean up pending request
  55. this.pendingRequests.delete(normalizedUrl);
  56. }
  57. }
  58. /**
  59. * Fetch metadata from main process via IPC with retry logic
  60. * @private
  61. * @param {string} url - Normalized video URL
  62. * @param {number} retryCount - Current retry attempt (default: 0)
  63. * @returns {Promise<Object>} Metadata object
  64. */
  65. async fetchMetadata(url, retryCount = 0) {
  66. if (!this.ipcAvailable) {
  67. console.warn('IPC not available, returning fallback metadata');
  68. return this.getFallbackMetadata(url);
  69. }
  70. try {
  71. // Create timeout promise
  72. const timeoutPromise = new Promise((_, reject) => {
  73. setTimeout(() => reject(new Error('Metadata fetch timeout')), this.timeout);
  74. });
  75. // Race between fetch and timeout
  76. const metadata = await Promise.race([
  77. window.IPCManager.getVideoMetadata(url),
  78. timeoutPromise
  79. ]);
  80. // Validate and normalize metadata
  81. return this.normalizeMetadata(metadata, url);
  82. } catch (error) {
  83. console.error(`Error fetching metadata for ${url} (attempt ${retryCount + 1}/${this.maxRetries + 1}):`, error);
  84. // Retry if we haven't exceeded max retries
  85. if (retryCount < this.maxRetries) {
  86. console.log(`Retrying metadata fetch for ${url} in ${this.retryDelay}ms...`);
  87. // Wait before retrying
  88. await new Promise(resolve => setTimeout(resolve, this.retryDelay));
  89. // Recursive retry
  90. return this.fetchMetadata(url, retryCount + 1);
  91. }
  92. // Return fallback metadata after all retries exhausted
  93. console.warn(`All retry attempts exhausted for ${url}, using fallback`);
  94. return this.getFallbackMetadata(url);
  95. }
  96. }
  97. /**
  98. * Normalize metadata response
  99. * @private
  100. * @param {Object} metadata - Raw metadata from IPC
  101. * @param {string} url - Original URL
  102. * @returns {Object} Normalized metadata
  103. */
  104. normalizeMetadata(metadata, url) {
  105. return {
  106. title: metadata.title || this.extractTitleFromUrl(url),
  107. thumbnail: metadata.thumbnail || null,
  108. duration: this.formatDuration(metadata.duration) || '00:00',
  109. filesize: metadata.filesize || null,
  110. uploader: metadata.uploader || null,
  111. uploadDate: metadata.upload_date || null,
  112. description: metadata.description || null,
  113. viewCount: metadata.view_count || null,
  114. likeCount: metadata.like_count || null
  115. };
  116. }
  117. /**
  118. * Get fallback metadata when fetch fails
  119. * @private
  120. * @param {string} url - Video URL
  121. * @returns {Object} Fallback metadata
  122. */
  123. getFallbackMetadata(url) {
  124. return {
  125. title: this.extractTitleFromUrl(url),
  126. thumbnail: null,
  127. duration: '00:00',
  128. filesize: null,
  129. uploader: null,
  130. uploadDate: null,
  131. description: null,
  132. viewCount: null,
  133. likeCount: null
  134. };
  135. }
  136. /**
  137. * Extract title from URL as fallback
  138. * @private
  139. * @param {string} url - Video URL
  140. * @returns {string} Extracted or placeholder title
  141. */
  142. extractTitleFromUrl(url) {
  143. try {
  144. // Extract video ID for YouTube
  145. if (url.includes('youtube.com') || url.includes('youtu.be')) {
  146. const match = url.match(/(?:v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/);
  147. if (match) {
  148. return `YouTube Video (${match[1]})`;
  149. }
  150. }
  151. // Extract video ID for Vimeo
  152. if (url.includes('vimeo.com')) {
  153. const match = url.match(/vimeo\.com\/(\d+)/);
  154. if (match) {
  155. return `Vimeo Video (${match[1]})`;
  156. }
  157. }
  158. return url;
  159. } catch (error) {
  160. return url;
  161. }
  162. }
  163. /**
  164. * Format duration from seconds to MM:SS or HH:MM:SS
  165. * @private
  166. * @param {number} seconds - Duration in seconds
  167. * @returns {string} Formatted duration string
  168. */
  169. formatDuration(seconds) {
  170. if (!seconds || isNaN(seconds)) {
  171. return '00:00';
  172. }
  173. const hours = Math.floor(seconds / 3600);
  174. const minutes = Math.floor((seconds % 3600) / 60);
  175. const secs = Math.floor(seconds % 60);
  176. if (hours > 0) {
  177. return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  178. } else {
  179. return `${minutes}:${secs.toString().padStart(2, '0')}`;
  180. }
  181. }
  182. /**
  183. * Normalize URL for caching
  184. * @private
  185. * @param {string} url - Video URL
  186. * @returns {string} Normalized URL
  187. */
  188. normalizeUrl(url) {
  189. if (window.URLValidator) {
  190. return window.URLValidator.normalizeUrl(url);
  191. }
  192. return url.trim();
  193. }
  194. /**
  195. * Clear cache for specific URL or all URLs
  196. * @param {string} [url] - Optional URL to clear from cache
  197. */
  198. clearCache(url = null) {
  199. if (url) {
  200. const normalizedUrl = this.normalizeUrl(url);
  201. this.cache.delete(normalizedUrl);
  202. } else {
  203. this.cache.clear();
  204. }
  205. }
  206. /**
  207. * Get cache statistics
  208. * @returns {Object} Cache stats
  209. */
  210. getCacheStats() {
  211. return {
  212. size: this.cache.size,
  213. pendingRequests: this.pendingRequests.size,
  214. urls: Array.from(this.cache.keys())
  215. };
  216. }
  217. /**
  218. * Prefetch metadata for multiple URLs
  219. * @param {string[]} urls - Array of URLs to prefetch
  220. * @returns {Promise<Object[]>} Array of metadata objects
  221. */
  222. async prefetchMetadata(urls) {
  223. if (!Array.isArray(urls)) {
  224. throw new Error('URLs must be an array');
  225. }
  226. const promises = urls.map(url =>
  227. this.getVideoMetadata(url).catch(error => {
  228. console.warn(`Failed to prefetch metadata for ${url}:`, error);
  229. return this.getFallbackMetadata(url);
  230. })
  231. );
  232. return Promise.all(promises);
  233. }
  234. }
  235. // Create singleton instance
  236. const metadataService = new MetadataService();
  237. // Export for use in other modules
  238. if (typeof module !== 'undefined' && module.exports) {
  239. module.exports = metadataService;
  240. } else if (typeof window !== 'undefined') {
  241. window.MetadataService = metadataService;
  242. }