Video.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. // GrabZilla 2.1 - Video Model
  2. // Core data structure for video management
  3. class Video {
  4. constructor(url, options = {}) {
  5. this.id = this.generateId();
  6. this.url = this.validateUrl(url);
  7. this.title = options.title || 'Loading...';
  8. this.thumbnail = options.thumbnail || 'assets/icons/placeholder.svg';
  9. this.duration = options.duration || '00:00';
  10. // Use current app state defaults if no options provided
  11. const appState = window.appState || window.app?.state;
  12. const defaultQuality = appState?.config?.defaultQuality || window.AppConfig?.APP_CONFIG?.DEFAULT_QUALITY || '1080p';
  13. const defaultFormat = appState?.config?.defaultFormat || window.AppConfig?.APP_CONFIG?.DEFAULT_FORMAT || 'None';
  14. this.quality = options.quality || defaultQuality;
  15. this.format = options.format || defaultFormat;
  16. this.status = options.status || 'ready';
  17. this.progress = options.progress || 0;
  18. this.filename = options.filename || '';
  19. this.error = options.error || null;
  20. this.retryCount = options.retryCount || 0;
  21. this.maxRetries = options.maxRetries || 3;
  22. this.downloadSpeed = options.downloadSpeed || null;
  23. this.eta = options.eta || null;
  24. this.isFetchingMetadata = options.isFetchingMetadata !== undefined ? options.isFetchingMetadata : false;
  25. this.requiresAuth = options.requiresAuth || false; // Video requires cookie file for download
  26. this.createdAt = new Date();
  27. this.updatedAt = new Date();
  28. }
  29. // Generate unique ID for video
  30. generateId() {
  31. return 'video_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
  32. }
  33. // Validate and normalize URL
  34. validateUrl(url) {
  35. if (!url || typeof url !== 'string') {
  36. throw new Error('Invalid URL provided');
  37. }
  38. const trimmedUrl = url.trim();
  39. if (window.URLValidator && !window.URLValidator.isValidVideoUrl(trimmedUrl)) {
  40. throw new Error('Invalid video URL format');
  41. }
  42. return trimmedUrl;
  43. }
  44. // Update video properties
  45. update(properties) {
  46. const allowedProperties = [
  47. 'title', 'thumbnail', 'duration', 'quality', 'format',
  48. 'status', 'progress', 'filename', 'error', 'retryCount', 'maxRetries', 'downloadSpeed', 'eta', 'isFetchingMetadata', 'requiresAuth'
  49. ];
  50. Object.keys(properties).forEach(key => {
  51. if (allowedProperties.includes(key)) {
  52. this[key] = properties[key];
  53. }
  54. });
  55. this.updatedAt = new Date();
  56. return this;
  57. }
  58. // Get video display name
  59. getDisplayName() {
  60. return this.title !== 'Loading...' ? this.title : this.url;
  61. }
  62. // Check if video is downloadable
  63. isDownloadable() {
  64. return this.status === 'ready' && !this.error;
  65. }
  66. // Check if video is currently processing
  67. isProcessing() {
  68. return ['downloading', 'converting', 'paused'].includes(this.status);
  69. }
  70. // Check if video is paused
  71. isPaused() {
  72. return this.status === 'paused';
  73. }
  74. // Check if video is completed
  75. isCompleted() {
  76. return this.status === 'completed';
  77. }
  78. // Check if video has error
  79. hasError() {
  80. return this.status === 'error' || !!this.error;
  81. }
  82. // Reset video for re-download (useful if file was deleted)
  83. resetForRedownload() {
  84. this.status = 'ready';
  85. this.progress = 0;
  86. this.error = null;
  87. this.filename = '';
  88. this.updatedAt = new Date();
  89. return this;
  90. }
  91. // Get formatted duration
  92. getFormattedDuration() {
  93. if (!this.duration || this.duration === '00:00') {
  94. return 'Unknown';
  95. }
  96. return this.duration;
  97. }
  98. // Get status display text
  99. getStatusText() {
  100. switch (this.status) {
  101. case 'ready':
  102. return 'Ready';
  103. case 'downloading':
  104. return this.progress > 0 ? `Downloading ${this.progress}%` : 'Downloading';
  105. case 'converting':
  106. return this.progress > 0 ? `Converting ${this.progress}%` : 'Converting';
  107. case 'completed':
  108. return 'Completed';
  109. case 'error':
  110. return 'Error';
  111. default:
  112. return this.status;
  113. }
  114. }
  115. // Get progress percentage as integer
  116. getProgressPercent() {
  117. return Math.max(0, Math.min(100, Math.round(this.progress || 0)));
  118. }
  119. // Check if video supports the specified quality
  120. supportsQuality(quality) {
  121. const supportedQualities = window.AppConfig?.APP_CONFIG?.SUPPORTED_QUALITIES ||
  122. ['720p', '1080p', '1440p', '4K'];
  123. return supportedQualities.includes(quality);
  124. }
  125. // Check if video supports the specified format
  126. supportsFormat(format) {
  127. const supportedFormats = window.AppConfig?.APP_CONFIG?.SUPPORTED_FORMATS ||
  128. ['None', 'H264', 'ProRes', 'DNxHR', 'Audio only'];
  129. return supportedFormats.includes(format);
  130. }
  131. // Get video platform (YouTube, Vimeo, etc.)
  132. getPlatform() {
  133. if (window.URLValidator) {
  134. return window.URLValidator.getPlatform(this.url);
  135. }
  136. return 'Unknown';
  137. }
  138. // Get normalized URL
  139. getNormalizedUrl() {
  140. if (window.URLValidator) {
  141. return window.URLValidator.normalizeUrl(this.url);
  142. }
  143. return this.url;
  144. }
  145. // Get estimated file size (if available from metadata)
  146. getEstimatedFileSize() {
  147. // This would be populated from video metadata
  148. return this.estimatedSize || null;
  149. }
  150. // Get download speed (if currently downloading)
  151. getDownloadSpeed() {
  152. return this.downloadSpeed || null;
  153. }
  154. // Get time remaining (if currently processing)
  155. getTimeRemaining() {
  156. if (!this.isProcessing() || !this.progress || this.progress === 0) {
  157. return null;
  158. }
  159. const speed = this.getDownloadSpeed();
  160. if (!speed) return null;
  161. const remainingPercent = 100 - this.progress;
  162. const estimatedSeconds = (remainingPercent / this.progress) * (this.getElapsedTime() / 1000);
  163. return this.formatDuration(estimatedSeconds);
  164. }
  165. // Get elapsed time since creation or status change
  166. getElapsedTime() {
  167. return Date.now() - this.updatedAt.getTime();
  168. }
  169. // Format duration from seconds
  170. formatDuration(seconds) {
  171. if (!seconds || seconds < 0) return '00:00';
  172. const hours = Math.floor(seconds / 3600);
  173. const minutes = Math.floor((seconds % 3600) / 60);
  174. const secs = Math.floor(seconds % 60);
  175. if (hours > 0) {
  176. return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  177. } else {
  178. return `${minutes}:${secs.toString().padStart(2, '0')}`;
  179. }
  180. }
  181. // Convert to JSON for storage/transmission
  182. toJSON() {
  183. return {
  184. id: this.id,
  185. url: this.url,
  186. title: this.title,
  187. thumbnail: this.thumbnail,
  188. duration: this.duration,
  189. quality: this.quality,
  190. format: this.format,
  191. status: this.status,
  192. progress: this.progress,
  193. filename: this.filename,
  194. error: this.error,
  195. isFetchingMetadata: this.isFetchingMetadata,
  196. requiresAuth: this.requiresAuth,
  197. estimatedSize: this.estimatedSize,
  198. downloadSpeed: this.downloadSpeed,
  199. createdAt: this.createdAt.toISOString(),
  200. updatedAt: this.updatedAt.toISOString()
  201. };
  202. }
  203. // Create Video from JSON
  204. static fromJSON(data) {
  205. const video = new Video(data.url, {
  206. title: data.title,
  207. thumbnail: data.thumbnail,
  208. duration: data.duration,
  209. quality: data.quality,
  210. format: data.format,
  211. status: data.status,
  212. progress: data.progress,
  213. filename: data.filename,
  214. error: data.error,
  215. isFetchingMetadata: data.isFetchingMetadata || false,
  216. requiresAuth: data.requiresAuth || false
  217. });
  218. video.id = data.id;
  219. video.estimatedSize = data.estimatedSize;
  220. video.downloadSpeed = data.downloadSpeed;
  221. video.createdAt = new Date(data.createdAt);
  222. video.updatedAt = new Date(data.updatedAt);
  223. return video;
  224. }
  225. // Create Video from URL with metadata
  226. static fromUrl(url, options = {}) {
  227. try {
  228. const video = new Video(url, options);
  229. video.isFetchingMetadata = true;
  230. // Fetch metadata in background (non-blocking for instant UI update)
  231. if (window.MetadataService) {
  232. window.MetadataService.getVideoMetadata(url)
  233. .then(metadata => {
  234. const oldProperties = { ...video };
  235. video.update({
  236. title: metadata.title,
  237. thumbnail: metadata.thumbnail,
  238. duration: metadata.duration,
  239. estimatedSize: metadata.filesize,
  240. isFetchingMetadata: false
  241. });
  242. // Notify AppState that video was updated so UI can re-render
  243. const appState = window.appState || window.app?.state;
  244. if (appState && appState.emit) {
  245. appState.emit('videoUpdated', { video, oldProperties });
  246. }
  247. })
  248. .catch(metadataError => {
  249. console.warn('Failed to fetch metadata for video:', metadataError.message);
  250. // Check if error indicates authentication is required
  251. const errorMsg = metadataError.message.toLowerCase();
  252. const requiresAuth = errorMsg.includes('sign in') ||
  253. errorMsg.includes('age') ||
  254. errorMsg.includes('restricted') ||
  255. errorMsg.includes('private') ||
  256. errorMsg.includes('members');
  257. const oldProperties = { ...video };
  258. video.update({
  259. title: video.url,
  260. isFetchingMetadata: false,
  261. requiresAuth: requiresAuth
  262. });
  263. // Notify AppState even on error so UI updates
  264. const appState = window.appState || window.app?.state;
  265. if (appState && appState.emit) {
  266. appState.emit('videoUpdated', { video, oldProperties });
  267. }
  268. });
  269. }
  270. // Return immediately - don't wait for metadata
  271. return video;
  272. } catch (error) {
  273. throw new Error(`Failed to create video from URL: ${error.message}`);
  274. }
  275. }
  276. // Clone video with new properties
  277. clone(overrides = {}) {
  278. const cloned = Video.fromJSON(this.toJSON());
  279. if (Object.keys(overrides).length > 0) {
  280. cloned.update(overrides);
  281. }
  282. return cloned;
  283. }
  284. // Compare two videos for equality
  285. equals(other) {
  286. if (!(other instanceof Video)) {
  287. return false;
  288. }
  289. return this.getNormalizedUrl() === other.getNormalizedUrl();
  290. }
  291. // Get video summary for logging/debugging
  292. getSummary() {
  293. return {
  294. id: this.id,
  295. title: this.getDisplayName(),
  296. url: this.url,
  297. status: this.status,
  298. progress: this.progress,
  299. quality: this.quality,
  300. format: this.format,
  301. platform: this.getPlatform()
  302. };
  303. }
  304. }
  305. // Export for use in other modules
  306. if (typeof module !== 'undefined' && module.exports) {
  307. // Node.js environment
  308. module.exports = Video;
  309. } else {
  310. // Browser environment - attach to window
  311. window.Video = Video;
  312. }