Initial commit - SoundWave v1.0

- Full PWA support with offline capabilities
- Comprehensive search across songs, playlists, and channels
- Offline playlist manager with download tracking
- Pre-built frontend for zero-build deployment
- Docker-based deployment with docker compose
- Material-UI dark theme interface
- YouTube audio download and management
- Multi-user authentication support
This commit is contained in:
Iulian 2025-12-16 23:43:07 +00:00
commit 51679d1943
254 changed files with 37281 additions and 0 deletions

View file

@ -0,0 +1,377 @@
/* eslint-disable no-restricted-globals */
const CACHE_NAME = 'soundwave-v1';
const API_CACHE_NAME = 'soundwave-api-v1';
const AUDIO_CACHE_NAME = 'soundwave-audio-v1';
const IMAGE_CACHE_NAME = 'soundwave-images-v1';
// Assets to cache on install
const STATIC_ASSETS = [
'/',
'/index.html',
'/manifest.json',
'/favicon.ico',
];
// Install event - cache static assets
self.addEventListener('install', (event) => {
console.log('[Service Worker] Installing...');
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
console.log('[Service Worker] Caching static assets');
return cache.addAll(STATIC_ASSETS).catch((err) => {
console.error('[Service Worker] Failed to cache static assets:', err);
});
}).then(() => {
console.log('[Service Worker] Installed');
return self.skipWaiting(); // Activate immediately
})
);
});
// Activate event - clean up old caches
self.addEventListener('activate', (event) => {
console.log('[Service Worker] Activating...');
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (
cacheName !== CACHE_NAME &&
cacheName !== API_CACHE_NAME &&
cacheName !== AUDIO_CACHE_NAME &&
cacheName !== IMAGE_CACHE_NAME
) {
console.log('[Service Worker] Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
}).then(() => {
console.log('[Service Worker] Activated');
return self.clients.claim(); // Take control immediately
})
);
});
// Fetch event - implement caching strategies
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// Skip chrome extensions and non-http(s) requests
if (!url.protocol.startsWith('http')) {
return;
}
// API requests - Network first, fallback to cache
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstStrategy(request, API_CACHE_NAME));
return;
}
// Audio files - Cache first, fallback to network
if (
url.pathname.includes('/audio/') ||
url.pathname.includes('/media/local_audio/') ||
request.destination === 'audio'
) {
event.respondWith(cacheFirstStrategy(request, AUDIO_CACHE_NAME));
return;
}
// Images - Cache first, fallback to network
if (
url.pathname.includes('/img/') ||
url.pathname.includes('/media/') ||
url.pathname.includes('thumbnail') ||
url.pathname.includes('cover') ||
request.destination === 'image'
) {
event.respondWith(cacheFirstStrategy(request, IMAGE_CACHE_NAME));
return;
}
// Static assets (JS, CSS) - Stale while revalidate
if (
request.destination === 'script' ||
request.destination === 'style' ||
url.pathname.endsWith('.js') ||
url.pathname.endsWith('.css')
) {
event.respondWith(staleWhileRevalidateStrategy(request, CACHE_NAME));
return;
}
// HTML pages - Network first, fallback to cache
if (request.mode === 'navigate' || request.destination === 'document') {
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
return;
}
// Default - Network first
event.respondWith(networkFirstStrategy(request, CACHE_NAME));
});
// Network first strategy - try network, fallback to cache
async function networkFirstStrategy(request, cacheName) {
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.log('[Service Worker] Network request failed, trying cache:', request.url);
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
// Return offline page for navigation requests
if (request.mode === 'navigate') {
const cache = await caches.open(CACHE_NAME);
const offlinePage = await cache.match('/index.html');
if (offlinePage) {
return offlinePage;
}
}
throw error;
}
}
// Cache first strategy - try cache, fallback to network
async function cacheFirstStrategy(request, cacheName) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
return cachedResponse;
}
try {
const networkResponse = await fetch(request);
// Cache successful responses
if (networkResponse && networkResponse.status === 200) {
const cache = await caches.open(cacheName);
cache.put(request, networkResponse.clone());
}
return networkResponse;
} catch (error) {
console.error('[Service Worker] Cache and network failed:', error);
throw error;
}
}
// Stale while revalidate - return cache immediately, update in background
async function staleWhileRevalidateStrategy(request, cacheName) {
const cachedResponse = await caches.match(request);
const fetchPromise = fetch(request).then((networkResponse) => {
if (networkResponse && networkResponse.status === 200) {
const cache = caches.open(cacheName);
cache.then((c) => c.put(request, networkResponse.clone()));
}
return networkResponse;
}).catch((error) => {
console.log('[Service Worker] Background fetch failed:', error);
});
return cachedResponse || fetchPromise;
}
// Background sync for offline uploads
self.addEventListener('sync', (event) => {
console.log('[Service Worker] Background sync:', event.tag);
if (event.tag === 'sync-audio-uploads') {
event.waitUntil(syncAudioUploads());
}
if (event.tag === 'sync-favorites') {
event.waitUntil(syncFavorites());
}
});
async function syncAudioUploads() {
console.log('[Service Worker] Syncing audio uploads...');
// Implementation for syncing pending uploads when back online
// This would read from IndexedDB and upload pending files
}
async function syncFavorites() {
console.log('[Service Worker] Syncing favorites...');
// Implementation for syncing favorite changes when back online
}
// Push notifications
self.addEventListener('push', (event) => {
console.log('[Service Worker] Push notification received');
const data = event.data ? event.data.json() : {};
const title = data.title || 'SoundWave';
const options = {
body: data.body || 'New content available',
icon: '/img/icon-192x192.png',
badge: '/img/icon-72x72.png',
vibrate: [200, 100, 200],
data: data.url || '/',
actions: [
{ action: 'open', title: 'Open' },
{ action: 'close', title: 'Close' },
],
};
event.waitUntil(
self.registration.showNotification(title, options)
);
});
// Notification click
self.addEventListener('notificationclick', (event) => {
console.log('[Service Worker] Notification clicked');
event.notification.close();
if (event.action === 'open' || !event.action) {
const urlToOpen = event.notification.data || '/';
event.waitUntil(
clients.openWindow(urlToOpen)
);
}
});
// Message handling for cache management
self.addEventListener('message', (event) => {
console.log('[Service Worker] Message received:', event.data);
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting();
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => caches.delete(cacheName))
);
}).then(() => {
event.ports[0].postMessage({ success: true });
})
);
}
if (event.data && event.data.type === 'CACHE_AUDIO') {
const { url } = event.data;
event.waitUntil(
caches.open(AUDIO_CACHE_NAME).then((cache) => {
return cache.add(url);
}).then(() => {
event.ports[0].postMessage({ success: true });
}).catch((error) => {
event.ports[0].postMessage({ success: false, error: error.message });
})
);
}
// Cache playlist for offline access with authentication
if (event.data && event.data.type === 'CACHE_PLAYLIST') {
const { playlistId, audioUrls } = event.data;
event.waitUntil(
(async () => {
try {
console.log('[Service Worker] Caching playlist:', playlistId, 'with', audioUrls.length, 'tracks');
const results = {
metadata: false,
audioFiles: [],
failed: []
};
// Cache playlist metadata API response (includes items)
try {
const apiCache = await caches.open(API_CACHE_NAME);
const metadataUrl = `/api/playlist/${playlistId}/?include_items=true`;
await apiCache.add(metadataUrl);
results.metadata = true;
console.log('[Service Worker] Cached playlist metadata');
} catch (err) {
console.warn('[Service Worker] Failed to cache playlist metadata:', err);
}
// Cache all audio files in playlist with authentication
const audioCache = await caches.open(AUDIO_CACHE_NAME);
for (const url of audioUrls) {
try {
// Create authenticated request
const authRequest = new Request(url, {
credentials: 'include',
headers: {
'Accept': 'audio/*',
}
});
const response = await fetch(authRequest);
if (response.ok) {
// Clone and cache the response
await audioCache.put(url, response.clone());
results.audioFiles.push(url);
console.log('[Service Worker] Cached audio:', url);
} else {
results.failed.push(url);
console.warn('[Service Worker] Failed to cache audio (status ' + response.status + '):', url);
}
} catch (err) {
results.failed.push(url);
console.warn('[Service Worker] Failed to cache audio:', url, err);
}
}
console.log('[Service Worker] Playlist caching complete:', results);
event.ports[0].postMessage({
success: results.audioFiles.length > 0,
metadata: results.metadata,
cached: results.audioFiles.length,
failed: results.failed.length,
details: results
});
} catch (error) {
console.error('[Service Worker] Playlist caching error:', error);
event.ports[0].postMessage({ success: false, error: error.message });
}
})()
);
}
// Remove cached playlist
if (event.data && event.data.type === 'REMOVE_PLAYLIST_CACHE') {
const { playlistId, audioUrls } = event.data;
event.waitUntil(
Promise.all([
// Remove playlist metadata from cache
caches.open(API_CACHE_NAME).then((cache) => {
return cache.delete(`/api/playlist/${playlistId}/`);
}),
// Remove audio files from cache (only if not used by other playlists)
caches.open(AUDIO_CACHE_NAME).then((cache) => {
return Promise.all(
audioUrls.map(url => cache.delete(url))
);
})
]).then(() => {
event.ports[0].postMessage({ success: true });
}).catch((error) => {
event.ports[0].postMessage({ success: false, error: error.message });
})
);
}
});
console.log('[Service Worker] Loaded');