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() {