3 Commits
Author SHA1 Message Date
SansanandClaude Opus 5.5 36329ca5cc Release 1.3.0
Build and Release / build (push) Successful in 31s
Build and Release / release (push) Successful in 3m22s
- Send the play queue ID (X-Queue-Id) with every request, and keep the
  queue across page reloads.
- Bypass browser caches for random songs.
- Start the sync time at 00:00, to match the server's minutes and seconds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:10:34 +02:00
SansanandClaude Opus 5.5 9c72f89703 Start the sync time at 00:00
Build and Release / build (push) Successful in 35s
Build and Release / release (push) Successful in 6s
The server now sends sync times as minutes and seconds, so the placeholder
shown before the first progress update matches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 22:09:32 +02:00
Sansan f26438abe0 Send the play queue ID to the server
Build and Release / build (push) Successful in 33s
Build and Release / release (push) Successful in 9s
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.
2026-10-07 20:09:31 +02:00
5 changed files with 97 additions and 49 deletions

No files matched your search

+1 -1
View File
@@ -25,7 +25,7 @@ The Docker image writes `config.js` from `public/config.template.js` when the co
| Variable | Description | | 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. | | `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): For `npm run serve`, create `public/config.js` (ignored by git):
+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "1.2.0", "version": "1.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "1.2.0", "version": "1.3.0",
"dependencies": { "dependencies": {
"axios": "^1.7.2", "axios": "^1.7.2",
"core-js": "^3.37.1", "core-js": "^3.37.1",
+46 -44
View File
@@ -1,55 +1,57 @@
{ {
"name": "music-randomizer-vue", "name": "music-randomizer-vue",
"version": "1.2.0", "version": "1.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.2", "axios": "^1.7.2",
"core-js": "^3.37.1", "core-js": "^3.37.1",
"vue": "^3.4.31", "vue": "^3.4.31",
"vue-axios": "^3.5.2", "vue-axios": "^3.5.2",
"vuex": "^4.1.0" "vuex": "^4.1.0"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "^5.0.8", "@vue/cli-plugin-babel": "^5.0.8",
"@vue/cli-plugin-eslint": "^5.0.8", "@vue/cli-plugin-eslint": "^5.0.8",
"@vue/cli-service": "^5.0.8", "@vue/cli-service": "^5.0.8",
"@vue/compiler-sfc": "^3.4.31", "@vue/compiler-sfc": "^3.4.31",
"@babel/eslint-parser": "^7.25.1", "@babel/eslint-parser": "^7.25.1",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-vue": "^9.27.0" "eslint-plugin-vue": "^9.27.0"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,
"env": { "env": {
"node": true "node": true
}, },
"extends": [ "extends": [
"plugin:vue/vue3-essential", "plugin:vue/vue3-essential",
"eslint:recommended" "eslint:recommended"
], ],
"parserOptions": { "parserOptions": {
"parser": "@babel/eslint-parser", "parser": "@babel/eslint-parser",
"requireConfigFile": false, "requireConfigFile": false,
"ecmaVersion": 2020, "ecmaVersion": 2020,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": { "ecmaFeatures": {
"globalReturn": false, "globalReturn": false,
"impliedStrict": true, "impliedStrict": true,
"jsx": true "jsx": true
}, },
"babelOptions": { "babelOptions": {
"presets": ["@babel/preset-env"] "presets": [
} "@babel/preset-env"
}, ]
"rules": { }
"no-debugger": 1 },
} "rules": {
}, "no-debugger": 1
}
},
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
+46
View File
@@ -12,6 +12,35 @@ export const api = axios.create({ baseURL: `${hostname}/api/v1` });
// Client for the server routes outside /api/v1 (the /find search). // Client for the server routes outside /api/v1 (the /find search).
export const legacyApi = axios.create({ baseURL: hostname }); 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 token = null;
let expiresAt = 0; let expiresAt = 0;
let pendingToken = null; let pendingToken = null;
@@ -45,9 +74,26 @@ api.interceptors.request.use(async (request) => {
if (!isTokenRequest(request)) { if (!isTokenRequest(request)) {
request.headers.Authorization = `Bearer ${await getToken()}`; 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; 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): // On 401 the token is no longer valid (for example after a server restart):
// get a new one and retry the request once. // get a new one and retry the request once.
api.interceptors.response.use(undefined, (error) => { api.interceptors.response.use(undefined, (error) => {
+2 -2
View File
@@ -81,7 +81,7 @@ export default {
data() { data() {
return { return {
progress: 0, progress: 0,
timeSpent: "00:00:00", timeSpent: "00:00",
syncComplete: false, syncComplete: false,
syncData: { syncData: {
soundtracks_added: [], soundtracks_added: [],
@@ -181,7 +181,7 @@ export default {
if (this.syncInProgress) { if (this.syncInProgress) {
// New sync in progress - start polling // New sync in progress - start polling
this.progress = 0; this.progress = 0;
this.timeSpent = "00:00:00"; this.timeSpent = "00:00";
this.syncComplete = false; this.syncComplete = false;
this.syncData = { this.syncData = {
soundtracks_added: [], soundtracks_added: [],