| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361 |
- // GrabZilla 2.1 - Video Model
- // Core data structure for video management
- class Video {
- constructor(url, options = {}) {
- this.id = this.generateId();
- this.url = this.validateUrl(url);
- this.title = options.title || 'Loading...';
- this.thumbnail = options.thumbnail || 'assets/icons/placeholder.svg';
- this.duration = options.duration || '00:00';
- // Use current app state defaults if no options provided
- const appState = window.appState || window.app?.state;
- const defaultQuality = appState?.config?.defaultQuality || window.AppConfig?.APP_CONFIG?.DEFAULT_QUALITY || '1080p';
- const defaultFormat = appState?.config?.defaultFormat || window.AppConfig?.APP_CONFIG?.DEFAULT_FORMAT || 'None';
- this.quality = options.quality || defaultQuality;
- this.format = options.format || defaultFormat;
- this.status = options.status || 'ready';
- this.progress = options.progress || 0;
- this.filename = options.filename || '';
- this.error = options.error || null;
- this.retryCount = options.retryCount || 0;
- this.maxRetries = options.maxRetries || 3;
- this.downloadSpeed = options.downloadSpeed || null;
- this.eta = options.eta || null;
- this.isFetchingMetadata = options.isFetchingMetadata !== undefined ? options.isFetchingMetadata : false;
- this.requiresAuth = options.requiresAuth || false; // Video requires cookie file for download
- this.createdAt = new Date();
- this.updatedAt = new Date();
- }
- // Generate unique ID for video
- generateId() {
- return 'video_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
- }
- // Validate and normalize URL
- validateUrl(url) {
- if (!url || typeof url !== 'string') {
- throw new Error('Invalid URL provided');
- }
- const trimmedUrl = url.trim();
- if (window.URLValidator && !window.URLValidator.isValidVideoUrl(trimmedUrl)) {
- throw new Error('Invalid video URL format');
- }
- return trimmedUrl;
- }
- // Update video properties
- update(properties) {
- const allowedProperties = [
- 'title', 'thumbnail', 'duration', 'quality', 'format',
- 'status', 'progress', 'filename', 'error', 'retryCount', 'maxRetries', 'downloadSpeed', 'eta', 'isFetchingMetadata', 'requiresAuth'
- ];
- Object.keys(properties).forEach(key => {
- if (allowedProperties.includes(key)) {
- this[key] = properties[key];
- }
- });
- this.updatedAt = new Date();
- return this;
- }
- // Get video display name
- getDisplayName() {
- return this.title !== 'Loading...' ? this.title : this.url;
- }
- // Check if video is downloadable
- isDownloadable() {
- return this.status === 'ready' && !this.error;
- }
- // Check if video is currently processing
- isProcessing() {
- return ['downloading', 'converting', 'paused'].includes(this.status);
- }
- // Check if video is paused
- isPaused() {
- return this.status === 'paused';
- }
- // Check if video is completed
- isCompleted() {
- return this.status === 'completed';
- }
- // Check if video has error
- hasError() {
- return this.status === 'error' || !!this.error;
- }
- // Reset video for re-download (useful if file was deleted)
- resetForRedownload() {
- this.status = 'ready';
- this.progress = 0;
- this.error = null;
- this.filename = '';
- this.updatedAt = new Date();
- return this;
- }
- // Get formatted duration
- getFormattedDuration() {
- if (!this.duration || this.duration === '00:00') {
- return 'Unknown';
- }
- return this.duration;
- }
- // Get status display text
- getStatusText() {
- switch (this.status) {
- case 'ready':
- return 'Ready';
- case 'downloading':
- return this.progress > 0 ? `Downloading ${this.progress}%` : 'Downloading';
- case 'converting':
- return this.progress > 0 ? `Converting ${this.progress}%` : 'Converting';
- case 'completed':
- return 'Completed';
- case 'error':
- return 'Error';
- default:
- return this.status;
- }
- }
- // Get progress percentage as integer
- getProgressPercent() {
- return Math.max(0, Math.min(100, Math.round(this.progress || 0)));
- }
- // Check if video supports the specified quality
- supportsQuality(quality) {
- const supportedQualities = window.AppConfig?.APP_CONFIG?.SUPPORTED_QUALITIES ||
- ['720p', '1080p', '1440p', '4K'];
- return supportedQualities.includes(quality);
- }
- // Check if video supports the specified format
- supportsFormat(format) {
- const supportedFormats = window.AppConfig?.APP_CONFIG?.SUPPORTED_FORMATS ||
- ['None', 'H264', 'ProRes', 'DNxHR', 'Audio only'];
- return supportedFormats.includes(format);
- }
- // Get video platform (YouTube, Vimeo, etc.)
- getPlatform() {
- if (window.URLValidator) {
- return window.URLValidator.getPlatform(this.url);
- }
- return 'Unknown';
- }
- // Get normalized URL
- getNormalizedUrl() {
- if (window.URLValidator) {
- return window.URLValidator.normalizeUrl(this.url);
- }
- return this.url;
- }
- // Get estimated file size (if available from metadata)
- getEstimatedFileSize() {
- // This would be populated from video metadata
- return this.estimatedSize || null;
- }
- // Get download speed (if currently downloading)
- getDownloadSpeed() {
- return this.downloadSpeed || null;
- }
- // Get time remaining (if currently processing)
- getTimeRemaining() {
- if (!this.isProcessing() || !this.progress || this.progress === 0) {
- return null;
- }
- const speed = this.getDownloadSpeed();
- if (!speed) return null;
- const remainingPercent = 100 - this.progress;
- const estimatedSeconds = (remainingPercent / this.progress) * (this.getElapsedTime() / 1000);
- return this.formatDuration(estimatedSeconds);
- }
- // Get elapsed time since creation or status change
- getElapsedTime() {
- return Date.now() - this.updatedAt.getTime();
- }
- // Format duration from seconds
- formatDuration(seconds) {
- if (!seconds || seconds < 0) return '00:00';
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- const secs = Math.floor(seconds % 60);
- if (hours > 0) {
- return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
- } else {
- return `${minutes}:${secs.toString().padStart(2, '0')}`;
- }
- }
- // Convert to JSON for storage/transmission
- toJSON() {
- return {
- id: this.id,
- url: this.url,
- title: this.title,
- thumbnail: this.thumbnail,
- duration: this.duration,
- quality: this.quality,
- format: this.format,
- status: this.status,
- progress: this.progress,
- filename: this.filename,
- error: this.error,
- isFetchingMetadata: this.isFetchingMetadata,
- requiresAuth: this.requiresAuth,
- estimatedSize: this.estimatedSize,
- downloadSpeed: this.downloadSpeed,
- createdAt: this.createdAt.toISOString(),
- updatedAt: this.updatedAt.toISOString()
- };
- }
- // Create Video from JSON
- static fromJSON(data) {
- const video = new Video(data.url, {
- title: data.title,
- thumbnail: data.thumbnail,
- duration: data.duration,
- quality: data.quality,
- format: data.format,
- status: data.status,
- progress: data.progress,
- filename: data.filename,
- error: data.error,
- isFetchingMetadata: data.isFetchingMetadata || false,
- requiresAuth: data.requiresAuth || false
- });
- video.id = data.id;
- video.estimatedSize = data.estimatedSize;
- video.downloadSpeed = data.downloadSpeed;
- video.createdAt = new Date(data.createdAt);
- video.updatedAt = new Date(data.updatedAt);
- return video;
- }
- // Create Video from URL with metadata
- static fromUrl(url, options = {}) {
- try {
- const video = new Video(url, options);
- video.isFetchingMetadata = true;
- // Fetch metadata in background (non-blocking for instant UI update)
- if (window.MetadataService) {
- window.MetadataService.getVideoMetadata(url)
- .then(metadata => {
- const oldProperties = { ...video };
- video.update({
- title: metadata.title,
- thumbnail: metadata.thumbnail,
- duration: metadata.duration,
- estimatedSize: metadata.filesize,
- isFetchingMetadata: false
- });
- // Notify AppState that video was updated so UI can re-render
- const appState = window.appState || window.app?.state;
- if (appState && appState.emit) {
- appState.emit('videoUpdated', { video, oldProperties });
- }
- })
- .catch(metadataError => {
- console.warn('Failed to fetch metadata for video:', metadataError.message);
- // Check if error indicates authentication is required
- const errorMsg = metadataError.message.toLowerCase();
- const requiresAuth = errorMsg.includes('sign in') ||
- errorMsg.includes('age') ||
- errorMsg.includes('restricted') ||
- errorMsg.includes('private') ||
- errorMsg.includes('members');
- const oldProperties = { ...video };
- video.update({
- title: video.url,
- isFetchingMetadata: false,
- requiresAuth: requiresAuth
- });
- // Notify AppState even on error so UI updates
- const appState = window.appState || window.app?.state;
- if (appState && appState.emit) {
- appState.emit('videoUpdated', { video, oldProperties });
- }
- });
- }
- // Return immediately - don't wait for metadata
- return video;
- } catch (error) {
- throw new Error(`Failed to create video from URL: ${error.message}`);
- }
- }
- // Clone video with new properties
- clone(overrides = {}) {
- const cloned = Video.fromJSON(this.toJSON());
- if (Object.keys(overrides).length > 0) {
- cloned.update(overrides);
- }
- return cloned;
- }
- // Compare two videos for equality
- equals(other) {
- if (!(other instanceof Video)) {
- return false;
- }
- return this.getNormalizedUrl() === other.getNormalizedUrl();
- }
- // Get video summary for logging/debugging
- getSummary() {
- return {
- id: this.id,
- title: this.getDisplayName(),
- url: this.url,
- status: this.status,
- progress: this.progress,
- quality: this.quality,
- format: this.format,
- platform: this.getPlatform()
- };
- }
- }
- // Export for use in other modules
- if (typeof module !== 'undefined' && module.exports) {
- // Node.js environment
- module.exports = Video;
- } else {
- // Browser environment - attach to window
- window.Video = Video;
- }
|