Video.js 11 KB

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