/** * Watch Progress Utilities * Centralized constants and helper functions for watch progress calculations */ /** * Completion threshold - content is marked as "completed" when reaching this percentage */ export const COMPLETION_THRESHOLD = 0.9; // 90% /** * Minimum progress threshold - don't save progress below this to avoid accidental saves */ export const MIN_PROGRESS_THRESHOLD = 0.05; // 5% /** * Significant position change threshold in seconds * Used to detect seeks and trigger saves */ export const SIGNIFICANT_POSITION_CHANGE = 30; // 30 seconds /** * Backup save interval in milliseconds * How often to save progress during continuous playback */ export const BACKUP_SAVE_INTERVAL = 30000; // 30 seconds /** * Calculate progress percentage from current position and total duration * @param currentSeconds Current playback position in seconds * @param totalSeconds Total content duration in seconds * @returns Progress percentage (0-100) */ export function calculateProgressPercentage( currentSeconds: number, totalSeconds: number ): number { if (!totalSeconds || totalSeconds <= 0) return 0; const percentage = (currentSeconds / totalSeconds) * 100; // Round to 2 decimal places and clamp between 0-100 return Math.min(100, Math.max(0, Math.round(percentage * 100) / 100)); } /** * Check if content is considered completed based on progress percentage * @param percentage Progress percentage (0-100) * @returns true if content is completed */ export function isCompleted(percentage: number): boolean { return percentage >= COMPLETION_THRESHOLD * 100; } /** * Check if content is considered completed based on position and duration * @param currentSeconds Current playback position in seconds * @param totalSeconds Total content duration in seconds * @returns true if content is completed */ export function isContentCompleted( currentSeconds: number, totalSeconds: number ): boolean { if (!totalSeconds || totalSeconds <= 0) return false; return currentSeconds / totalSeconds >= COMPLETION_THRESHOLD; } /** * Check if progress should be saved based on current percentage * @param percentage Progress percentage (0-100) * @returns true if progress should be saved */ export function shouldSaveProgress(percentage: number): boolean { return percentage >= MIN_PROGRESS_THRESHOLD * 100; } /** * Check if progress should be saved based on position and duration * @param currentSeconds Current playback position in seconds * @param totalSeconds Total content duration in seconds * @returns true if progress should be saved */ export function shouldSaveProgressByPosition( currentSeconds: number, totalSeconds: number ): boolean { if (!totalSeconds || totalSeconds <= 0) return false; return currentSeconds / totalSeconds >= MIN_PROGRESS_THRESHOLD; } /** * Format seconds into human-readable time string * @param seconds Duration in seconds * @returns Formatted time string (e.g., "1h 23m", "45m", "2m 30s") */ export function formatDuration(seconds: number): string { if (!seconds || seconds < 0) return '0m'; const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = Math.floor(seconds % 60); if (hours > 0) { if (minutes > 0) { return `${hours}h ${minutes}m`; } return `${hours}h`; } if (minutes > 0) { if (remainingSeconds > 0 && minutes < 10) { return `${minutes}m ${remainingSeconds}s`; } return `${minutes}m`; } return `${remainingSeconds}s`; } /** * Format remaining time for continue watching display * @param remainingSeconds Remaining duration in seconds * @returns Formatted string (e.g., "23 min left", "1 hr 10 min left") */ export function formatRemainingTime(remainingSeconds: number): string { if (!remainingSeconds || remainingSeconds < 0) return ''; const hours = Math.floor(remainingSeconds / 3600); const minutes = Math.floor((remainingSeconds % 3600) / 60); if (hours > 0) { if (minutes > 0) { return `${hours} hr ${minutes} min left`; } return `${hours} hr left`; } if (minutes > 0) { return `${minutes} min left`; } return 'Less than 1 min left'; } /** * Calculate remaining time from progress and total duration * @param currentSeconds Current playback position in seconds * @param totalSeconds Total content duration in seconds * @returns Remaining seconds */ export function calculateRemainingTime( currentSeconds: number, totalSeconds: number ): number { if (!totalSeconds || totalSeconds <= 0) return 0; return Math.max(0, totalSeconds - currentSeconds); } /** * Generate a unique key for progress entry (for Map lookups) * @param contentId Content ID * @param episodeId Optional episode ID for series * @returns Unique key string */ export function getProgressKey(contentId: string, episodeId?: string): string { return episodeId ? `${contentId}:${episodeId}` : contentId; } /** * Generate a unique ID for a progress entry * @param contentId Content ID * @param episodeId Optional episode ID for series * @returns Unique ID string */ export function generateProgressId(contentId: string, episodeId?: string): string { return episodeId ? `${contentId}-${episodeId}` : contentId; } /** * Check if position change is significant enough to trigger a save * @param previousPosition Previous playback position in seconds * @param currentPosition Current playback position in seconds * @returns true if change is significant (likely a seek) */ export function isSignificantPositionChange( previousPosition: number, currentPosition: number ): boolean { return Math.abs(currentPosition - previousPosition) > SIGNIFICANT_POSITION_CHANGE; }