diff --git a/background.png b/background.png new file mode 100644 index 0000000..ee6c39b Binary files /dev/null and b/background.png differ diff --git a/index.html b/index.html index c9f0319..7769a77 100644 --- a/index.html +++ b/index.html @@ -26,8 +26,8 @@ background: linear-gradient(145deg, #2a2a2a 0%, #1a1a1a 50%, #0f0f0f 100%); border: 8px solid #0a0a0a; box-shadow: - inset 0 0 0 2px #3a3a3a, - inset 0 0 50px rgba(0,0,0,0.9), + /* inset 0 0 0 2px #3a3a3a, */ + inset 0 0 10px rgba(0,0,0,0.1), 0 30px 60px rgba(0,0,0,0.8), 5px 5px 0 rgba(0,0,0,0.3); /* Extra top padding to accommodate eject/lightning buttons */ @@ -44,31 +44,15 @@ left: 0; right: 0; bottom: 0; - background-image: - linear-gradient(45deg, transparent 48%, rgba(255,255,255,0.03) 49%, rgba(255,255,255,0.03) 51%, transparent 52%), - linear-gradient(-45deg, transparent 48%, rgba(0,0,0,0.4) 49%, rgba(0,0,0,0.4) 51%, transparent 52%), - repeating-linear-gradient(90deg, transparent, transparent 3px, rgba(0,0,0,0.5) 3px, rgba(0,0,0,0.5) 4px), - url('data:image/svg+xml,'); + background-image: url('background.png'); + background-size: 100%; + background-position: center; + background-repeat: repeat; pointer-events: none; - opacity: 0.8; - mix-blend-mode: overlay; + opacity: 0.33; + mix-blend-mode: normal; } - /* Rust and corrosion patches */ - .player::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-image: - radial-gradient(ellipse at 20% 30%, rgba(139, 69, 19, 0.3) 0%, transparent 40%), - radial-gradient(ellipse at 80% 70%, rgba(101, 67, 33, 0.2) 0%, transparent 35%), - radial-gradient(ellipse at 60% 10%, rgba(120, 81, 45, 0.25) 0%, transparent 30%), - radial-gradient(ellipse at 10% 90%, rgba(139, 90, 43, 0.2) 0%, transparent 40%); - pointer-events: none; - } /* Eject button - positioned top-left of player */ .eject-btn { @@ -1306,6 +1290,7 @@ function getTrackDuration(url) { return new Promise((resolve, reject) => { const tempAudio = new Audio(); + tempAudio.preload = 'metadata'; // Only fetch headers, not entire file // Set timeout for slow loads const timeout = setTimeout(() => { @@ -1326,13 +1311,8 @@ reject(e); }); - // Try to use cached URL if available, otherwise use direct URL - TrackCache.getTrack(url).then(blobUrl => { - tempAudio.src = blobUrl; - }).catch(() => { - // Fall back to direct URL - tempAudio.src = url; - }); + // Direct URL - browser handles caching via HTTP headers + tempAudio.src = url; }); } @@ -1608,196 +1588,6 @@ // SOUND EFFECTS MODULE - END // ======================================== - // ======================================== - // TRACK CACHE MODULE - START - // Caches audio files locally using Cache API - // Uses HEAD requests to detect content changes - // ======================================== - const TrackCache = { - CACHE_NAME: 'cassette-player-audio-v1', - META_KEY: 'track-cache-meta', - // Fallback cache duration (1 hour) if no ETag/Last-Modified - FALLBACK_MAX_AGE: 60 * 60 * 1000, - cache: null, - - /** - * Initialize the cache - opens Cache API storage - * @returns {Promise} - */ - async init() { - if (!this.cache) { - this.cache = await caches.open(this.CACHE_NAME); - } - return this.cache; - }, - - /** - * Get metadata for all cached tracks from localStorage - * @returns {Object} Map of URL -> { etag, lastModified, cachedAt } - */ - getMetadata() { - try { - const data = localStorage.getItem(this.META_KEY); - return data ? JSON.parse(data) : {}; - } catch (e) { - console.warn('TrackCache: Failed to read metadata', e); - return {}; - } - }, - - /** - * Save metadata for a track - * @param {string} url - Track URL - * @param {Object} meta - { etag, lastModified, cachedAt } - */ - saveMetadata(url, meta) { - try { - const allMeta = this.getMetadata(); - allMeta[url] = meta; - localStorage.setItem(this.META_KEY, JSON.stringify(allMeta)); - } catch (e) { - console.warn('TrackCache: Failed to save metadata', e); - } - }, - - /** - * Check if cached content is stale using HEAD request - * @param {string} url - Track URL - * @returns {Promise} True if content has changed, false if still valid - */ - async isStale(url) { - const meta = this.getMetadata()[url]; - if (!meta) return true; // No metadata = treat as stale - - try { - const response = await fetch(url, { method: 'HEAD' }); - - if (!response.ok) { - // HEAD failed, assume cached version is still valid - console.log('TrackCache: HEAD request failed, using cached version'); - return false; - } - - const newEtag = response.headers.get('ETag'); - const newLastModified = response.headers.get('Last-Modified'); - - // Check ETag first (most reliable) - if (newEtag && meta.etag) { - const isChanged = newEtag !== meta.etag; - if (isChanged) { - console.log(`TrackCache: ETag changed for ${url}`); - } - return isChanged; - } - - // Fall back to Last-Modified - if (newLastModified && meta.lastModified) { - const isChanged = newLastModified !== meta.lastModified; - if (isChanged) { - console.log(`TrackCache: Last-Modified changed for ${url}`); - } - return isChanged; - } - - // No ETag or Last-Modified, use time-based fallback - const age = Date.now() - meta.cachedAt; - const isExpired = age > this.FALLBACK_MAX_AGE; - if (isExpired) { - console.log(`TrackCache: Cache expired (age: ${Math.round(age/1000)}s) for ${url}`); - } - return isExpired; - - } catch (e) { - // Network error or CORS issue - assume cached version is valid - console.log('TrackCache: HEAD request error, using cached version', e.message); - return false; - } - }, - - /** - * Fetch track from network and store in cache - * @param {string} url - Track URL - * @returns {Promise} Blob URL for the audio - */ - async fetchAndCache(url) { - console.log(`TrackCache: Fetching ${url}`); - - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`Failed to fetch track: ${response.status}`); - } - - // Extract cache validation headers - const etag = response.headers.get('ETag'); - const lastModified = response.headers.get('Last-Modified'); - - // Clone response before consuming it - const responseToCache = response.clone(); - - // Store in cache - await this.init(); - await this.cache.put(url, responseToCache); - - // Save metadata - this.saveMetadata(url, { - etag: etag, - lastModified: lastModified, - cachedAt: Date.now() - }); - - console.log(`TrackCache: Cached ${url} (ETag: ${etag || 'none'}, Last-Modified: ${lastModified || 'none'})`); - - // Return blob URL for playback - const blob = await response.blob(); - return URL.createObjectURL(blob); - }, - - /** - * Get track - main entry point - * Checks cache, validates with HEAD, returns blob URL - * @param {string} url - Track URL - * @returns {Promise} Blob URL for the audio - */ - async getTrack(url) { - await this.init(); - - // Check if we have it cached - const cachedResponse = await this.cache.match(url); - - if (cachedResponse) { - // Check if content has changed - const stale = await this.isStale(url); - - if (!stale) { - console.log(`TrackCache: Using cached version of ${url}`); - const blob = await cachedResponse.blob(); - return URL.createObjectURL(blob); - } - - // Content changed, fetch new version - console.log(`TrackCache: Content changed, re-fetching ${url}`); - } - - // Not cached or stale, fetch from network - return await this.fetchAndCache(url); - }, - - /** - * Clear all cached tracks - * Useful for debugging or freeing storage - */ - async clearAll() { - await caches.delete(this.CACHE_NAME); - localStorage.removeItem(this.META_KEY); - this.cache = null; - console.log('TrackCache: Cleared all cached tracks'); - } - }; - // ======================================== - // TRACK CACHE MODULE - END - // ======================================== - // ======================================== // BUTTON CLICK SOUNDS - Add to all buttons // Uses mousedown for immediate tactile feedback @@ -1889,17 +1679,8 @@ // Reset scroll position for new track resetTitleScroll(); - try { - // Get track from cache or network - const blobUrl = await TrackCache.getTrack(playlist[index].url); - audio.src = blobUrl; - } catch (e) { - // Caching may fail due to CORS if audio is hosted on different origin - // without Access-Control-Allow-Origin headers - console.warn('TrackCache: Caching unavailable (likely CORS), using direct URL'); - // Fall back to direct URL - audio element can still play cross-origin - audio.src = playlist[index].url; - } + // Direct URL - browser handles caching via HTTP headers + audio.src = playlist[index].url; } // Initial track load (async, no await needed for initial load) @@ -1987,12 +1768,8 @@ currentTrack = 0; trackNameInner.textContent = playlist[0].name; - try { - const blobUrl = await TrackCache.getTrack(playlist[0].url); - audio.src = blobUrl; - } catch (e) { - audio.src = playlist[0].url; - } + // Direct URL - browser handles caching via HTTP headers + audio.src = playlist[0].url; audio.load(); } @@ -2123,12 +1900,8 @@ trackNameInner.textContent = playlist[trackIndex].name; resetTitleScroll(); - try { - const blobUrl = await TrackCache.getTrack(playlist[trackIndex].url); - audio.src = blobUrl; - } catch (e) { - audio.src = playlist[trackIndex].url; - } + // Direct URL - browser handles caching via HTTP headers + audio.src = playlist[trackIndex].url; return new Promise((resolve) => { audio.onloadedmetadata = () => { @@ -2256,13 +2029,8 @@ trackNameInner.textContent = playlist[currentTrack].name; resetTitleScroll(); - try { - const blobUrl = await TrackCache.getTrack(playlist[currentTrack].url); - audio.src = blobUrl; - } catch (e) { - console.warn('TrackCache: Caching unavailable, using direct URL'); - audio.src = playlist[currentTrack].url; - } + // Direct URL - browser handles caching via HTTP headers + audio.src = playlist[currentTrack].url; return new Promise(resolve => { audio.oncanplay = function() {