From cb5ea26c7772ff67e78b7fcfcdec75957b153b42 Mon Sep 17 00:00:00 2001 From: marc Date: Sat, 12 Sep 2026 14:34:17 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Maus-Zur=C3=BCck=20im=20PWA-Fenster,=20s?= =?UTF-8?q?prechende=20Player-Namen,=20Pfad-Etikett?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Im installierten Fenster ("display": standalone) gibt es keine Zurück-/ Vorwärts-Knöpfe, und die Maus-Seitentasten werden nicht auf den Verlauf umgelenkt — im Tab ging es, im Fenster nicht. Die App übernimmt X1/X2 jetzt selbst, aber nur im Standalone-Fenster (im Tab würde das doppelt navigieren; beide Fälle im Test abgedeckt). Zurück läuft über _navGoBack, schließt also erst ein offenes Now-Playing. Backend meldet sich jetzt als NovaSonic-Backend, der Scrobble-Dienst als NovaSonic-Scrobble. Vorher novasonic vs. NovaSonic — in Navidromes Player-Liste (Name = "client [User-Agent]") zwei Zeilen "[node]", die sich nur an einem Großbuchstaben unterschieden. Pfad-Etikett wieder ohne "(Navidrome)": der Wert kommt ohnehin aus der Subsonic-API, die Erklärung gehört in den Code und nicht ins Fenster. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 18 ++++++++++++++++++ backend/lib/romaji.js | 2 +- backend/novasonic-server.js | 16 ++++++++-------- scrobble/scrobble-server.js | 2 +- src/main.js | 19 +++++++++++++++++++ src/ui/metadata.js | 8 ++++---- 6 files changed, 51 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 857158f..8002bd7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,24 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d **Android:** 1.9 (versionCode 12) — Besitzer geteilter Playlists. ### Fixed +- **Zurück und Vorwärts gingen im installierten Fenster am PC gar nicht (Web)** — im Browser-Tab + funktionierten sie, im PWA-Fenster nicht. Das Manifest setzt `"display": "standalone"`: dort gibt + es keine Zurück-/Vorwärts-Knöpfe, und die Seitentasten der Maus werden nicht auf den Verlauf + umgelenkt. Die App übernimmt die Maustasten X1/X2 jetzt selbst — **nur** im Standalone-Fenster, + denn im Tab macht das der Browser und wir würden doppelt navigieren (beides mitgetestet). + Zurück geht über denselben Weg wie der In-App-Knopf, schließt also erst ein offenes + Now-Playing-Fenster. `Alt`+`←`/`→` bleibt dem Browser überlassen, das kann er auch im Fenster. +- **Zwei Node-Dienste hießen in Navidromes Player-Liste fast gleich** — das Backend meldete sich als + `novasonic`, der Scrobble-Dienst als `NovaSonic`; da Navidrome den Namen als `client [User-Agent]` + bildet, standen dort zwei Zeilen `novasonic [node]` und `NovaSonic [node]`, unterscheidbar nur an + einem Großbuchstaben. Genau die Zeile, an der „Report Real Path" hängt, war so nicht auffindbar. + Jetzt `NovaSonic-Backend` und `NovaSonic-Scrobble`. Navidrome legt dafür **neue** Player-Zeilen an; + die alten können weg. Eine Player-Zeile hält nur Client, User-Agent, IP, Transcoding, Bitratengrenze + und die beiden Schalter — keine Wiedergabezahlen, keine Historie. Löschen ist also gefahrlos, und + die neuen Zeilen erben `ND_SUBSONIC_DEFAULTREPORTREALPATH`. +- **„Pfad (Navidrome)" im Info-Fenster wieder zu „Pfad"** — der Zusatz war doppelt gemoppelt: der + Wert kommt ohnehin aus der Subsonic-API. Was er bedeutet, steht als Kommentar im Code und in den + Known Bugs, nicht als Etikett im Fenster. - **Der Zurück-Knopf tat oft nichts (Web)** — `navigate()` prüfte zwar, ob die Ansicht sich überhaupt geändert hat, und ließ den Verlaufseintrag dann weg — **rief `history.pushState` aber trotzdem**. Zweimal auf denselben Tab geklickt hieß also zwei Browser-Einträge auf dieselbe diff --git a/backend/lib/romaji.js b/backend/lib/romaji.js index 41f4670..e0ac5d5 100644 --- a/backend/lib/romaji.js +++ b/backend/lib/romaji.js @@ -115,7 +115,7 @@ const ROMAJI_TTL = 20 * 60 * 1000; // 20 min async function _fetchNdSongs(ndUrl, user, pass) { const salt = Math.random().toString(36).slice(2, 10); const token = createHash('md5').update(pass + salt).digest('hex'); - const base = `${ndUrl}/rest/search3.view?u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=novasonic&f=json&albumCount=0&artistCount=0`; + const base = `${ndUrl}/rest/search3.view?u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic-Backend&f=json&albumCount=0&artistCount=0`; const songs = []; let offset = 0; while (true) { diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index e4b27b3..4515d19 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -982,7 +982,7 @@ app.post('/api/fix-artist-sort', async (req, res) => { try { const { s, t } = mkTok(); const r = await fetch( - `${ndUrl}/rest/search3?query=${encodeURIComponent(artistKanji)}&artistCount=0&albumCount=0&songCount=500&u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=novasonic&f=json`, + `${ndUrl}/rest/search3?query=${encodeURIComponent(artistKanji)}&artistCount=0&albumCount=0&songCount=500&u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(15000) } ); const d = await r.json(); @@ -1104,7 +1104,7 @@ app.post('/api/fix-artist-sort', async (req, res) => { // Trigger Navidrome rescan try { const { s, t } = mkTok(); - await fetch(`${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=novasonic&f=json`, { signal: AbortSignal.timeout(5000) }); + await fetch(`${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(5000) }); } catch {} const firstErr = errorDetails[0] ? ` (${errorDetails[0]})` : ''; @@ -1247,7 +1247,7 @@ app.get('/api/song-sorttags', async (req, res) => { try { const salt = Math.random().toString(36).slice(2, 10); const token = crypto.createHash('md5').update(pass + salt).digest('hex'); - const ndRes = await fetch(`${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=novasonic&f=json`, { signal: AbortSignal.timeout(8000) }); + const ndRes = await fetch(`${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(8000) }); const nd = await ndRes.json(); filePath = nd?.['subsonic-response']?.song?.path; if (!filePath) return res.status(404).json({ error: 'Song not found' }); @@ -1290,7 +1290,7 @@ async function _ndApi(ndUrl, user, pass, method, extra = '') { const s = Math.random().toString(36).slice(2, 10); const t = crypto.createHash('md5').update(pass + s).digest('hex'); const r = await fetch( - `${ndUrl}/rest/${method}?u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=novasonic&f=json${extra}`, + `${ndUrl}/rest/${method}?u=${encodeURIComponent(user)}&t=${t}&s=${s}&v=1.16.1&c=NovaSonic-Backend&f=json${extra}`, { signal: AbortSignal.timeout(8000) } ); return r.json(); @@ -1377,7 +1377,7 @@ app.post('/api/edit-metadata', async (req, res) => { try { const salt = Math.random().toString(36).slice(2, 10); const token = crypto.createHash('md5').update(pass + salt).digest('hex'); - const ndApi = `${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=novasonic&f=json`; + const ndApi = `${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic-Backend&f=json`; const ndRes = await fetch(ndApi, { signal: AbortSignal.timeout(8000) }); const ndData = await ndRes.json(); ndSong = ndData?.['subsonic-response']?.song; @@ -1585,7 +1585,7 @@ app.post('/api/edit-metadata', async (req, res) => { try { const salt2 = Math.random().toString(36).slice(2, 10); const tok2 = crypto.createHash('md5').update(pass + salt2).digest('hex'); - await fetch(`${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${tok2}&s=${salt2}&v=1.16.1&c=novasonic&f=json`, { signal: AbortSignal.timeout(5000) }); + await fetch(`${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${tok2}&s=${salt2}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(5000) }); } catch {} // Restore playlist membership after rename/move — the rescan gives the renamed song a new ID @@ -1624,7 +1624,7 @@ app.post('/api/delete-song', async (req, res) => { const salt = Math.random().toString(36).slice(2, 10); const token = crypto.createHash('md5').update(pass + salt).digest('hex'); const ndRes = await fetch( - `${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=novasonic&f=json`, + `${ndUrl}/rest/getSong?id=${songId}&u=${encodeURIComponent(user)}&t=${token}&s=${salt}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(8000) } ); const ndData = await ndRes.json(); @@ -1688,7 +1688,7 @@ app.post('/api/delete-song', async (req, res) => { const s2 = Math.random().toString(36).slice(2, 10); const t2 = crypto.createHash('md5').update(pass + s2).digest('hex'); await fetch( - `${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${t2}&s=${s2}&v=1.16.1&c=novasonic&f=json`, + `${ndUrl}/rest/startScan?u=${encodeURIComponent(user)}&t=${t2}&s=${s2}&v=1.16.1&c=NovaSonic-Backend&f=json`, { signal: AbortSignal.timeout(5000) } ); } catch {} diff --git a/scrobble/scrobble-server.js b/scrobble/scrobble-server.js index 41afca4..cea916a 100644 --- a/scrobble/scrobble-server.js +++ b/scrobble/scrobble-server.js @@ -92,7 +92,7 @@ async function verifyUser(req) { if (cached && cached.exp > now) return cached.ok ? u : null; try { - const url = `${NAVIDROME_URL}/rest/ping.view?u=${encodeURIComponent(u)}&t=${encodeURIComponent(t)}&s=${encodeURIComponent(s)}&v=1.16.1&c=NovaSonic&f=json`; + const url = `${NAVIDROME_URL}/rest/ping.view?u=${encodeURIComponent(u)}&t=${encodeURIComponent(t)}&s=${encodeURIComponent(s)}&v=1.16.1&c=NovaSonic-Scrobble&f=json`; const r = await fetch(url, { signal: AbortSignal.timeout(8000) }); const j = await r.json(); const ok = j?.['subsonic-response']?.status === 'ok'; diff --git a/src/main.js b/src/main.js index 300a78f..575ab6f 100644 --- a/src/main.js +++ b/src/main.js @@ -316,6 +316,25 @@ window.addEventListener('load', () => { }); window.addEventListener('popstate', (e) => _navOnPopState(e.state)); + +// Im installierten Fenster ist "display": "standalone" — es gibt keine Zurueck-/Vorwaerts-Knoepfe, +// und die Maustasten werden dort nicht auf den Verlauf umgelenkt: Zurueck war im Programmfenster am +// PC also ueberhaupt nicht erreichbar, im Browser-Tab dagegen schon. Deshalb uebernehmen wir X1/X2 +// selbst — aber NUR im Standalone-Fenster, im Tab macht das der Browser und wir wuerden doppelt +// navigieren. (Alt+Links/Rechts bleibt dem Browser ueberlassen, das kann er auch dort selbst.) +const _imAppFenster = () => { + try { return window.matchMedia('(display-mode: standalone)').matches || navigator.standalone === true; } + catch { return false; } +}; +window.addEventListener('mouseup', (e) => { + if (!_imAppFenster()) return; + if (e.button === 3) { e.preventDefault(); _navGoBack(); } + else if (e.button === 4) { e.preventDefault(); try { history.forward(); } catch {} } +}); +// Ohne das zeigt Chrome im Fenster das Kontextmenue der Seitentaste. +window.addEventListener('auxclick', (e) => { + if (_imAppFenster() && (e.button === 3 || e.button === 4)) e.preventDefault(); +}); document.addEventListener('keydown', e => { if(e.key !== 'Backspace') return; const t = document.activeElement?.tagName; diff --git a/src/ui/metadata.js b/src/ui/metadata.js index e0b0013..d13be6d 100644 --- a/src/ui/metadata.js +++ b/src/ui/metadata.js @@ -38,10 +38,10 @@ function _renderInfoModal(s) { ['Bitrate', s.bitRate ? s.bitRate+' kbps' : null], ['Format', s.suffix?.toUpperCase()], ['Dateigröße', s.size ? Math.round(s.size/1024/1024*10)/10+' MB' : null], - // Navidrome gibt den echten Pfad nur heraus, wenn beim Player "Report Real Path" gesetzt ist; - // sonst baut es ihn per fakePath() aus den Tags: AlbumArtist/Album/DD-TT - Titel.ext. Das sah - // unbeschriftet nach einem kaputten Dateinamen aus, deshalb die Quellenangabe im Label. - ['Pfad (Navidrome)', s.path], + // Kommt so aus der Subsonic-API. Ohne "Report Real Path" am Player ist das NICHT der echte + // Dateiname, sondern von Navidrome aus den Tags gebaut (fakePath(): AlbumArtist/Album/ + // DD-TT - Titel.ext) — siehe CHANGELOG 2.4, Known Bugs. + ['Pfad', s.path], ['Plays', s.playCount], ].filter(([,v]) => v!=null && v!=='' && v!==undefined); document.getElementById('info-rows').innerHTML = fields.map(([l,v]) =>