shashwatIDR's picture
Upload 16 files
9bea604 verified
// Service Worker for VELIN Podcast Player
// Handles offline audio caching and playback
const CACHE_NAME = 'velin-audio-cache-v2';
const STATIC_CACHE_NAME = 'velin-static-v2';
const THUMBNAIL_CACHE_NAME = 'velin-thumbnails-v2';
// Static files to cache for offline functionality
const STATIC_FILES = [
'/',
'/manifest.json',
'/icon-192.png',
'/icon-512.png',
'/icon-192.svg',
'/icon-512.svg'
];
// Install event - cache static files and discover dynamic assets
self.addEventListener('install', event => {
console.log('VELIN Service Worker installed');
event.waitUntil(
cacheStaticFiles()
.then(() => {
console.log('All static files cached successfully');
})
.catch(error => {
console.error('Failed to cache static files:', error);
})
);
self.skipWaiting();
});
// Cache static files and discover dynamic assets
async function cacheStaticFiles() {
try {
const cache = await caches.open(STATIC_CACHE_NAME);
// First, cache the main HTML page to discover dynamic assets
const indexResponse = await fetch('/');
if (indexResponse.ok) {
await cache.put('/', indexResponse.clone());
// Parse HTML to find JS and CSS files
const htmlText = await indexResponse.text();
const dynamicAssets = extractAssetsFromHTML(htmlText);
console.log('Found dynamic assets:', dynamicAssets);
// Cache all discovered assets along with static files
const allFilesToCache = [...STATIC_FILES.filter(file => file !== '/'), ...dynamicAssets];
// Cache each file individually to handle failures gracefully
await Promise.allSettled(
allFilesToCache.map(async (file) => {
try {
const response = await fetch(file);
if (response.ok) {
await cache.put(file, response);
console.log('Cached:', file);
} else {
console.warn('Failed to fetch:', file, response.status);
}
} catch (error) {
console.warn('Error caching:', file, error);
}
})
);
}
} catch (error) {
console.error('Error in cacheStaticFiles:', error);
// Fallback: try to cache just the static files
const cache = await caches.open(STATIC_CACHE_NAME);
await Promise.allSettled(
STATIC_FILES.map(async (file) => {
try {
const response = await fetch(file);
if (response.ok) {
await cache.put(file, response);
}
} catch (error) {
console.warn('Fallback cache error for:', file);
}
})
);
}
}
// Extract JS and CSS files from HTML
function extractAssetsFromHTML(html) {
const assets = [];
// Find script tags with src
const scriptMatches = html.matchAll(/<script[^>]+src="([^"]+)"/g);
for (const match of scriptMatches) {
if (match[1] && !match[1].startsWith('http') && !match[1].includes('replit')) {
assets.push(match[1]);
}
}
// Find link tags with stylesheet
const linkMatches = html.matchAll(/<link[^>]+href="([^"]+)"[^>]*rel="stylesheet"/g);
for (const match of linkMatches) {
if (match[1] && !match[1].startsWith('http')) {
assets.push(match[1]);
}
}
return assets;
}
// Activate event - claim all clients and clean old caches
self.addEventListener('activate', event => {
console.log('VELIN Service Worker activated');
event.waitUntil(
Promise.all([
self.clients.claim(),
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
// Keep current caches, remove old versions
if (cacheName !== CACHE_NAME &&
cacheName !== STATIC_CACHE_NAME &&
cacheName !== THUMBNAIL_CACHE_NAME) {
console.log('Deleting old cache:', cacheName);
return caches.delete(cacheName);
}
})
);
})
])
);
});
// Fetch event - handle caching and serving
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// Handle audio files (.mp3, .m4a, .webm, .ogg)
if (isAudioFile(url.pathname) || isAudioStream(url.href)) {
event.respondWith(handleAudioRequest(event.request));
return;
}
// Handle API requests for audio downloads
if (url.pathname.startsWith('/api/download-audio/')) {
event.respondWith(handleAudioDownload(event.request));
return;
}
// Handle podcast stream requests
if (url.hostname === 'backendmix.vercel.app' && url.pathname.startsWith('/streams/')) {
event.respondWith(handleStreamRequest(event.request));
return;
}
// Handle thumbnail images
if (isThumbnailImage(url.href)) {
event.respondWith(handleThumbnailRequest(event.request));
return;
}
// Handle static files and navigation
if (event.request.method === 'GET') {
event.respondWith(handleStaticRequest(event.request));
return;
}
// Default handling for other requests
event.respondWith(fetch(event.request));
});
// Check if URL is an audio file
function isAudioFile(pathname) {
const audioExtensions = ['.mp3', '.m4a', '.webm', '.ogg', '.wav', '.aac'];
return audioExtensions.some(ext => pathname.endsWith(ext));
}
// Check if URL is an audio stream
function isAudioStream(url) {
// YouTube audio streams and other streaming services
return (
url.includes('googlevideo.com') ||
url.includes('youtube.com') ||
url.includes('ytimg.com') ||
(url.includes('mime=audio') || url.includes('type=audio'))
);
}
// Check if URL is a thumbnail image
function isThumbnailImage(url) {
return (
url.includes('ytimg.com') ||
url.includes('ggpht.com') ||
url.includes('youtube.com/vi/') ||
(url.includes('thumbnail') && (url.includes('.jpg') || url.includes('.webp') || url.includes('.png')))
);
}
// Handle thumbnail requests
async function handleThumbnailRequest(request) {
try {
const cache = await caches.open(THUMBNAIL_CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log('Serving thumbnail from cache:', request.url);
return cachedResponse;
}
// Fetch from network and cache
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
console.log('Thumbnail cached successfully:', request.url);
return networkResponse;
}
return networkResponse;
} catch (error) {
console.error('Error handling thumbnail request:', error);
// Return a fallback thumbnail when offline
const fallbackSvg = `
<svg viewBox="0 0 320 180" xmlns="http://www.w3.org/2000/svg">
<rect width="320" height="180" fill="#4B5563"/>
<g transform="translate(160, 90)">
<circle r="24" fill="#6366F1" opacity="0.2"/>
<path d="M-8 -6 L8 0 L-8 6 Z" fill="#6366F1"/>
</g>
<text x="160" y="135" text-anchor="middle" fill="#D1D5DB" font-family="sans-serif" font-size="12" font-weight="500">
VELIN
</text>
</svg>
`;
return new Response(fallbackSvg, {
headers: {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-cache'
}
});
}
}
// Handle static file requests (for offline app functionality)
async function handleStaticRequest(request) {
const url = new URL(request.url);
try {
// For navigation requests (page loads), handle specially for SPA
if (request.mode === 'navigate' ||
(request.destination === 'document' && request.method === 'GET')) {
// Try network first for navigation
try {
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache successful page loads
const cache = await caches.open(STATIC_CACHE_NAME);
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
console.log('Cached page from network:', request.url);
return networkResponse;
}
} catch (networkError) {
console.log('Network failed for navigation, using cache fallback');
}
// Network failed or offline - serve from cache
const cache = await caches.open(STATIC_CACHE_NAME);
// For SPA routing, always serve the main index.html for any route
const cachedResponse = await cache.match('/');
if (cachedResponse) {
console.log('Serving SPA fallback from cache for:', request.url);
return cachedResponse;
}
// No cached page available
console.error('No cached page available for offline access');
return new Response(`
<!DOCTYPE html>
<html>
<head>
<title>VELIN - Offline</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: system-ui, sans-serif;
text-align: center;
padding: 2rem;
background: #121212;
color: #ffffff;
margin: 0;
}
.container { max-width: 400px; margin: 0 auto; padding-top: 20vh; }
.icon { font-size: 4rem; margin-bottom: 1rem; }
h1 { color: #6366f1; margin-bottom: 1rem; }
p { color: #9ca3af; line-height: 1.5; }
</style>
</head>
<body>
<div class="container">
<div class="icon">📱</div>
<h1>VELIN</h1>
<p>You're offline and the app isn't cached yet.<br>Please connect to the internet and reload the page.</p>
</div>
</body>
</html>
`, {
headers: { 'Content-Type': 'text/html' },
status: 200
});
}
// For static resources (JS, CSS, images, etc.)
try {
const networkResponse = await fetch(request);
// Cache successful responses for static resources
if (networkResponse.ok && (
request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'image' ||
url.pathname.includes('.js') ||
url.pathname.includes('.css') ||
url.pathname.includes('.png') ||
url.pathname.includes('.svg') ||
url.pathname.includes('/assets/')
)) {
const cache = await caches.open(STATIC_CACHE_NAME);
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
console.log('Cached static resource:', request.url);
}
return networkResponse;
} catch (networkError) {
console.log('Network failed for static resource, trying cache:', request.url);
// Network failed, try cache as fallback
const cache = await caches.open(STATIC_CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log('Serving static resource from cache:', request.url);
return cachedResponse;
}
// No cached version available
throw networkError;
}
} catch (error) {
console.error('Error handling static request:', error);
return new Response('Resource not available offline', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
// Handle audio file requests
async function handleAudioRequest(request) {
try {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log('Serving audio from cache:', request.url);
return cachedResponse;
}
// Not in cache, fetch from network
console.log('Fetching audio from network:', request.url);
const networkResponse = await fetch(request);
if (networkResponse.ok) {
// Cache the response for future use
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
console.log('Audio cached successfully:', request.url);
}
return networkResponse;
} catch (error) {
console.error('Error handling audio request:', error);
// Return a fallback or error response
return new Response('Audio not available offline', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
// Handle audio download API requests
async function handleAudioDownload(request) {
try {
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(request);
if (cachedResponse) {
console.log('Serving download from cache:', request.url);
return cachedResponse;
}
// Fetch from network and cache
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const responseClone = networkResponse.clone();
await cache.put(request, responseClone);
console.log('Download cached successfully:', request.url);
}
return networkResponse;
} catch (error) {
console.error('Error handling download request:', error);
return new Response('Download not available offline', {
status: 503,
statusText: 'Service Unavailable'
});
}
}
// Handle stream info requests
async function handleStreamRequest(request) {
try {
const cache = await caches.open(STATIC_CACHE_NAME);
const cachedResponse = await cache.match(request);
// For stream info, we can cache for a short time
if (cachedResponse) {
const cacheDate = new Date(cachedResponse.headers.get('sw-cache-date') || 0);
const now = new Date();
const maxAge = 30 * 60 * 1000; // 30 minutes
if (now - cacheDate < maxAge) {
console.log('Serving stream info from cache:', request.url);
return cachedResponse;
}
}
// Fetch fresh data
const networkResponse = await fetch(request);
if (networkResponse.ok) {
const responseClone = networkResponse.clone();
const headers = new Headers(responseClone.headers);
headers.set('sw-cache-date', new Date().toISOString());
const cachedResponse = new Response(responseClone.body, {
status: responseClone.status,
statusText: responseClone.statusText,
headers: headers
});
await cache.put(request, cachedResponse);
}
return networkResponse;
} catch (error) {
console.error('Error handling stream request:', error);
return fetch(request);
}
}
// Message handling for manual cache operations
self.addEventListener('message', event => {
if (event.data && event.data.type === 'CACHE_AUDIO') {
event.waitUntil(cacheAudioFile(event.data.url));
}
if (event.data && event.data.type === 'CLEAR_CACHE') {
event.waitUntil(clearCache());
}
if (event.data && event.data.type === 'GET_CACHE_SIZE') {
event.waitUntil(getCacheSize().then(size => {
event.ports[0].postMessage({ size });
}));
}
});
// Manually cache an audio file
async function cacheAudioFile(url) {
try {
const cache = await caches.open(CACHE_NAME);
const response = await fetch(url);
if (response.ok) {
await cache.put(url, response);
console.log('Audio file cached manually:', url);
return true;
}
return false;
} catch (error) {
console.error('Error caching audio file:', error);
return false;
}
}
// Clear all caches
async function clearCache() {
try {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames.map(cacheName => caches.delete(cacheName))
);
console.log('All caches cleared');
} catch (error) {
console.error('Error clearing cache:', error);
}
}
// Get cache size
async function getCacheSize() {
try {
const cache = await caches.open(CACHE_NAME);
const requests = await cache.keys();
let totalSize = 0;
for (const request of requests) {
const response = await cache.match(request);
if (response) {
const blob = await response.blob();
totalSize += blob.size;
}
}
return totalSize;
} catch (error) {
console.error('Error calculating cache size:', error);
return 0;
}
}