From f26438abe048526051f9ae11d5df5f5ec2d4acf0 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Wed, 7 Oct 2026 20:09:31 +0200 Subject: [PATCH] Send the play queue ID to the server The server keeps a play queue per client, identified by the X-Queue-Id header. Send the stored ID with every request and save the ID from every response, so the client switches to the new queue after a reset or a sync. The ID is kept in localStorage so a page reload keeps the same queue. Also send Cache-Control: no-cache, so browsers that cached a random song before the server sent no-store fetch a new one. --- README.md | 2 +- src/api.js | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de831b7..149bf62 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The Docker image writes `config.js` from `public/config.template.js` when the co | Variable | Description | |---|---| -| `API_HOSTNAME` | URL of the MusicServer, for example `https://music.example.com`. Empty to use the same host as the frontend. | +| `API_HOSTNAME` | URL of the MusicServer, for example `https://music.example.com`. Empty to use the same host as the frontend. On another host, add the frontend's origin to the server's `CORS_ALLOWED_ORIGINS`. | | `API_PASSWORD` | The MusicServer's `API_PASSWORD`, used to get an API token. Empty if the server has no password. Anyone who can open the frontend can read it. | For `npm run serve`, create `public/config.js` (ignored by git): diff --git a/src/api.js b/src/api.js index 4574277..4e22832 100644 --- a/src/api.js +++ b/src/api.js @@ -12,6 +12,35 @@ export const api = axios.create({ baseURL: `${hostname}/api/v1` }); // Client for the server routes outside /api/v1 (the /find search). export const legacyApi = axios.create({ baseURL: hostname }); +// The server keeps a play queue per client, identified by this header. Every music +// response returns the queue's ID (a new one after a reset or a sync), and it is sent +// back with every request. It is stored so a page reload keeps the same queue. +const QUEUE_HEADER = "X-Queue-Id"; +const QUEUE_STORAGE_KEY = "queueId"; + +let queueId = readQueueId(); + +function readQueueId() { + try { + return localStorage.getItem(QUEUE_STORAGE_KEY); + } catch { + return null; + } +} + +function rememberQueueId(response) { + const id = response?.headers?.[QUEUE_HEADER.toLowerCase()]; + if (!id || id === queueId) { + return; + } + queueId = id; + try { + localStorage.setItem(QUEUE_STORAGE_KEY, id); + } catch { + // Without storage the queue still works until the page is reloaded. + } +} + let token = null; let expiresAt = 0; let pendingToken = null; @@ -45,9 +74,26 @@ api.interceptors.request.use(async (request) => { if (!isTokenRequest(request)) { request.headers.Authorization = `Bearer ${await getToken()}`; } + if (queueId) { + request.headers[QUEUE_HEADER] = queueId; + } + // Random songs come from the same URL every time. Browsers that cached one before + // the server sent Cache-Control: no-store would otherwise replay it. + request.headers["Cache-Control"] = "no-cache"; return request; }); +api.interceptors.response.use( + (response) => { + rememberQueueId(response); + return response; + }, + (error) => { + rememberQueueId(error.response); + return Promise.reject(error); + } +); + // On 401 the token is no longer valid (for example after a server restart): // get a new one and retry the request once. api.interceptors.response.use(undefined, (error) => {