Video.js 9.9 KB

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