diff --git a/server/player/index.html b/server/player/index.html
index 27065f6..db367b0 100644
--- a/server/player/index.html
+++ b/server/player/index.html
@@ -421,6 +421,7 @@
};
// Browser fingerprint (survives localStorage clear)
data.fingerprint = generateBrowserFingerprint();
+ console.log(`[register] device_id=${data.device_id || 'none'}, has_token=${!!data.device_token}, token_len=${data.device_token?.length || 0}, paired=${config.paired}, pairing_code=${data.pairing_code || 'none'}`);
socket.emit('device:register', data);
}
@@ -455,7 +456,10 @@
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => {
if (socket?.connected && config.deviceId && config.paired) {
- socket.emit('device:register', { device_id: config.deviceId, device_info: {} });
+ const data = { device_id: config.deviceId, device_info: {} };
+ if (config.deviceToken) data.device_token = config.deviceToken;
+ console.log(`[refresh-register] device_id=${config.deviceId}, has_token=${!!config.deviceToken}`);
+ socket.emit('device:register', data);
}
}, 300000); // 5 minutes fallback
}
diff --git a/server/player/sw.js b/server/player/sw.js
index b71f7cb..9db8fbc 100644
--- a/server/player/sw.js
+++ b/server/player/sw.js
@@ -1,4 +1,4 @@
-const CACHE_NAME = 'rd-player-v3';
+const CACHE_NAME = 'rd-player-v4';
const CONTENT_CACHE = 'rd-content-v1';
// Install: skip waiting to activate immediately
@@ -19,18 +19,39 @@ self.addEventListener('activate', (event) => {
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
- // Content files (videos, images): cache on first fetch for offline playback
+ // Only handle GET requests — let POST/PUT/DELETE pass through
+ if (event.request.method !== 'GET') return;
+
+ // Content files (videos, images): cache-first for offline playback
if (url.pathname.startsWith('/uploads/content/')) {
+ // Skip range requests (video seeking) — serve from network, don't cache partial responses
+ if (event.request.headers.get('range')) {
+ return; // Let the browser handle range requests directly
+ }
+
event.respondWith(
- caches.match(event.request).then(cached => {
- if (cached) return cached;
- return fetch(event.request).then(response => {
- if (response.ok) {
- const clone = response.clone();
- caches.open(CONTENT_CACHE).then(cache => cache.put(event.request, clone));
- }
- return response;
- }).catch(() => new Response('Offline', { status: 503 }));
+ caches.open(CONTENT_CACHE).then(cache =>
+ cache.match(event.request, { ignoreSearch: true }).then(cached => {
+ if (cached) return cached;
+ return fetch(event.request).then(response => {
+ // Only cache successful, complete (non-opaque) responses
+ if (response.ok && response.status === 200 && response.type !== 'opaque') {
+ cache.put(event.request, response.clone());
+ }
+ return response;
+ }).catch(() => {
+ return new Response('Content unavailable offline', {
+ status: 503,
+ statusText: 'Service Unavailable',
+ headers: { 'Content-Type': 'text/plain' }
+ });
+ });
+ })
+ ).catch(() => {
+ // Cache API itself failed — fall through to network
+ return fetch(event.request).catch(() =>
+ new Response('Offline', { status: 503, headers: { 'Content-Type': 'text/plain' } })
+ );
})
);
return;
@@ -40,16 +61,24 @@ self.addEventListener('fetch', (event) => {
if (url.pathname.startsWith('/player') || url.pathname === '/socket.io/socket.io.js') {
event.respondWith(
fetch(event.request).then(response => {
- if (response.ok) {
+ if (response.ok && response.type !== 'opaque') {
const clone = response.clone();
caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone));
}
return response;
- }).catch(() => caches.match(event.request).then(cached => cached || new Response('Offline', { status: 503 })))
+ }).catch(() =>
+ caches.match(event.request, { ignoreSearch: true }).then(cached =>
+ cached || new Response('Offline', {
+ status: 503,
+ statusText: 'Service Unavailable',
+ headers: { 'Content-Type': 'text/plain' }
+ })
+ )
+ )
);
return;
}
- // Everything else: network only
- event.respondWith(fetch(event.request));
+ // Everything else: network only, don't intercept failures
+ // (Returning without calling event.respondWith lets the browser handle it natively)
});
diff --git a/server/ws/deviceSocket.js b/server/ws/deviceSocket.js
index e8f3efc..0b1316a 100644
--- a/server/ws/deviceSocket.js
+++ b/server/ws/deviceSocket.js
@@ -182,7 +182,7 @@ module.exports = function setupDeviceSocket(io) {
if (device) {
// Validate device token (skip for legacy devices that don't have a token yet)
if (device.device_token && !validateDeviceToken(device_id, device_token)) {
- console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)}`);
+ console.warn(`Invalid device token for ${device_id} from ${getClientIp(socket)} — received_len=${(device_token || '').length}, stored_len=${device.device_token.length}, received_prefix=${(device_token || '').substring(0, 8)}, stored_prefix=${device.device_token.substring(0, 8)}`);
socket.emit('device:auth-error', { error: 'Invalid device token' });
return;
}