fix: Maus-Zurück im PWA-Fenster, sprechende Player-Namen, Pfad-Etikett
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
568bb5002c
commit
cb5ea26c77
6 files changed
+51
-14
No files matched your search
@@ -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.
|
**Android:** 1.9 (versionCode 12) — Besitzer geteilter Playlists.
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- **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
|
ü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
|
trotzdem**. Zweimal auf denselben Tab geklickt hieß also zwei Browser-Einträge auf dieselbe
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const ROMAJI_TTL = 20 * 60 * 1000; // 20 min
|
|||||||
async function _fetchNdSongs(ndUrl, user, pass) {
|
async function _fetchNdSongs(ndUrl, user, pass) {
|
||||||
const salt = Math.random().toString(36).slice(2, 10);
|
const salt = Math.random().toString(36).slice(2, 10);
|
||||||
const token = createHash('md5').update(pass + salt).digest('hex');
|
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 = [];
|
const songs = [];
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|||||||
@@ -982,7 +982,7 @@ app.post('/api/fix-artist-sort', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { s, t } = mkTok();
|
const { s, t } = mkTok();
|
||||||
const r = await fetch(
|
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) }
|
{ signal: AbortSignal.timeout(15000) }
|
||||||
);
|
);
|
||||||
const d = await r.json();
|
const d = await r.json();
|
||||||
@@ -1104,7 +1104,7 @@ app.post('/api/fix-artist-sort', async (req, res) => {
|
|||||||
// Trigger Navidrome rescan
|
// Trigger Navidrome rescan
|
||||||
try {
|
try {
|
||||||
const { s, t } = mkTok();
|
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 {}
|
} catch {}
|
||||||
|
|
||||||
const firstErr = errorDetails[0] ? ` (${errorDetails[0]})` : '';
|
const firstErr = errorDetails[0] ? ` (${errorDetails[0]})` : '';
|
||||||
@@ -1247,7 +1247,7 @@ app.get('/api/song-sorttags', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const salt = Math.random().toString(36).slice(2, 10);
|
const salt = Math.random().toString(36).slice(2, 10);
|
||||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
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();
|
const nd = await ndRes.json();
|
||||||
filePath = nd?.['subsonic-response']?.song?.path;
|
filePath = nd?.['subsonic-response']?.song?.path;
|
||||||
if (!filePath) return res.status(404).json({ error: 'Song not found' });
|
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 s = Math.random().toString(36).slice(2, 10);
|
||||||
const t = crypto.createHash('md5').update(pass + s).digest('hex');
|
const t = crypto.createHash('md5').update(pass + s).digest('hex');
|
||||||
const r = await fetch(
|
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) }
|
{ signal: AbortSignal.timeout(8000) }
|
||||||
);
|
);
|
||||||
return r.json();
|
return r.json();
|
||||||
@@ -1377,7 +1377,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const salt = Math.random().toString(36).slice(2, 10);
|
const salt = Math.random().toString(36).slice(2, 10);
|
||||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
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 ndRes = await fetch(ndApi, { signal: AbortSignal.timeout(8000) });
|
||||||
const ndData = await ndRes.json();
|
const ndData = await ndRes.json();
|
||||||
ndSong = ndData?.['subsonic-response']?.song;
|
ndSong = ndData?.['subsonic-response']?.song;
|
||||||
@@ -1585,7 +1585,7 @@ app.post('/api/edit-metadata', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const salt2 = Math.random().toString(36).slice(2, 10);
|
const salt2 = Math.random().toString(36).slice(2, 10);
|
||||||
const tok2 = crypto.createHash('md5').update(pass + salt2).digest('hex');
|
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 {}
|
} catch {}
|
||||||
|
|
||||||
// Restore playlist membership after rename/move — the rescan gives the renamed song a new ID
|
// 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 salt = Math.random().toString(36).slice(2, 10);
|
||||||
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
const token = crypto.createHash('md5').update(pass + salt).digest('hex');
|
||||||
const ndRes = await fetch(
|
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) }
|
{ signal: AbortSignal.timeout(8000) }
|
||||||
);
|
);
|
||||||
const ndData = await ndRes.json();
|
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 s2 = Math.random().toString(36).slice(2, 10);
|
||||||
const t2 = crypto.createHash('md5').update(pass + s2).digest('hex');
|
const t2 = crypto.createHash('md5').update(pass + s2).digest('hex');
|
||||||
await fetch(
|
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) }
|
{ signal: AbortSignal.timeout(5000) }
|
||||||
);
|
);
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ async function verifyUser(req) {
|
|||||||
if (cached && cached.exp > now) return cached.ok ? u : null;
|
if (cached && cached.exp > now) return cached.ok ? u : null;
|
||||||
|
|
||||||
try {
|
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 r = await fetch(url, { signal: AbortSignal.timeout(8000) });
|
||||||
const j = await r.json();
|
const j = await r.json();
|
||||||
const ok = j?.['subsonic-response']?.status === 'ok';
|
const ok = j?.['subsonic-response']?.status === 'ok';
|
||||||
|
|||||||
+19
@@ -316,6 +316,25 @@ window.addEventListener('load', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('popstate', (e) => _navOnPopState(e.state));
|
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 => {
|
document.addEventListener('keydown', e => {
|
||||||
if(e.key !== 'Backspace') return;
|
if(e.key !== 'Backspace') return;
|
||||||
const t = document.activeElement?.tagName;
|
const t = document.activeElement?.tagName;
|
||||||
|
|||||||
+4
-4
@@ -38,10 +38,10 @@ function _renderInfoModal(s) {
|
|||||||
['Bitrate', s.bitRate ? s.bitRate+' kbps' : null],
|
['Bitrate', s.bitRate ? s.bitRate+' kbps' : null],
|
||||||
['Format', s.suffix?.toUpperCase()],
|
['Format', s.suffix?.toUpperCase()],
|
||||||
['Dateigröße', s.size ? Math.round(s.size/1024/1024*10)/10+' MB' : null],
|
['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;
|
// Kommt so aus der Subsonic-API. Ohne "Report Real Path" am Player ist das NICHT der echte
|
||||||
// sonst baut es ihn per fakePath() aus den Tags: AlbumArtist/Album/DD-TT - Titel.ext. Das sah
|
// Dateiname, sondern von Navidrome aus den Tags gebaut (fakePath(): AlbumArtist/Album/
|
||||||
// unbeschriftet nach einem kaputten Dateinamen aus, deshalb die Quellenangabe im Label.
|
// DD-TT - Titel.ext) — siehe CHANGELOG 2.4, Known Bugs.
|
||||||
['Pfad (Navidrome)', s.path],
|
['Pfad', s.path],
|
||||||
['Plays', s.playCount],
|
['Plays', s.playCount],
|
||||||
].filter(([,v]) => v!=null && v!=='' && v!==undefined);
|
].filter(([,v]) => v!=null && v!=='' && v!==undefined);
|
||||||
document.getElementById('info-rows').innerHTML = fields.map(([l,v]) =>
|
document.getElementById('info-rows').innerHTML = fields.map(([l,v]) =>
|
||||||
|
|||||||
Reference in new issue
Block a user