# All files / src/services subtitleCache.ts

0% Statements 0/54
0% Branches 0/22
0% Functions 0/15
0% Lines 0/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169

     /**
     * Subtitle Cache Service
     *
     * Provides caching and preloading functionality for subtitle files
     * to enable instant subtitle switching without delays.
     */

    interface CachedSubtitle {
      url: string;
      content: string;
      timestamp: number;
    }

    class SubtitleCacheService {
      private cache: Map<string, CachedSubtitle> = new Map();
      private maxCacheSize: number = 50; // Maximum number of subtitles to cache
      private maxCacheAge: number = 1000 * 60 * 30; // 30 minutes

      /**
       * Preload all subtitles for a content item
       */
      async preloadSubtitles(subtitles: Array<{ url: string; language: string; label: string }>): Promise<void> {
        const promises = subtitles.map(sub => this.fetchAndCache(sub.url));
        await Promise.all(promises).catch(() => {});
      }

      /**
       * Fetch and cache a subtitle file
       */
      private async fetchAndCache(url: string): Promise<string> {
        // Check if already cached and not expired
        const cached = this.cache.get(url);
        if (cached && (Date.now() - cached.timestamp) < this.maxCacheAge) {
          return cached.content;
        }

        const response = await fetch(url, {
          credentials: 'include',
          headers: {
            'Accept': 'text/vtt,text/plain,*/*'}});

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }

        const content = await response.text();

        // Store in cache
        this.cache.set(url, {
          url,
          content,
          timestamp: Date.now()});

        // Enforce cache size limit
        this.enforceMaxCacheSize();

        return content;
      }

      /**
       * Get a subtitle from cache or fetch it
       */
      async getSubtitle(url: string): Promise<string> {
        return this.fetchAndCache(url);
      }

      /**
       * Check if a subtitle is cached
       */
      isCached(url: string): boolean {
        const cached = this.cache.get(url);
        if (!cached) return false;

        // Check if expired
        if ((Date.now() - cached.timestamp) >= this.maxCacheAge) {
          this.cache.delete(url);
          return false;
        }

        return true;
      }

      /**
       * Get cached subtitle content without fetching
       */
      getCachedContent(url: string): string | null {
        const cached = this.cache.get(url);
        if (!cached) return null;

        // Check if expired
        if ((Date.now() - cached.timestamp) >= this.maxCacheAge) {
          this.cache.delete(url);
          return null;
        }

        return cached.content;
      }

      /**
       * Create a blob URL from cached subtitle content
       */
      createBlobUrl(url: string): string | null {
        const content = this.getCachedContent(url);
        if (!content) return null;

        const blob = new Blob([content], { type: 'text/vtt' });
        return URL.createObjectURL(blob);
      }

      /**
       * Enforce maximum cache size by removing oldest entries
       */
      private enforceMaxCacheSize(): void {
        if (this.cache.size <= this.maxCacheSize) return;

        // Sort by timestamp (oldest first)
        const entries = Array.from(this.cache.entries()).sort(
          (a, b) => a[1].timestamp - b[1].timestamp
        );

        // Remove oldest entries until we're under the limit
        const toRemove = entries.slice(0, this.cache.size - this.maxCacheSize);
        toRemove.forEach(([url]) => {
          this.cache.delete(url);
        });
      }

      /**
       * Clear all cached subtitles
       */
      clearCache(): void {
        this.cache.clear();
      }

      /**
       * Clear expired cache entries
       */
      clearExpired(): void {
        const now = Date.now();

        for (const [url, cached] of this.cache.entries()) {
          if ((now - cached.timestamp) >= this.maxCacheAge) {
            this.cache.delete(url);
          }
        }
      }

      /**
       * Get cache statistics
       */
      getStats(): { size: number; maxSize: number; urls: string[] } {
        return {
          size: this.cache.size,
          maxSize: this.maxCacheSize,
          urls: Array.from(this.cache.keys())};
      }
    }

    // Export singleton instance
    export const subtitleCache = new SubtitleCacheService();

    // Periodically clear expired entries (every 5 minutes)
    if (typeof window !== 'undefined') {
      setInterval(() => {
        subtitleCache.clearExpired();
      }, 1000 * 60 * 5);
    }