feat(web): refresh the current view after deleting a song or playlist

Deletes left the removed item visible because the list loaders are cached (getPlaylists
~3 min): deletePlaylist navigated to a still-cached list, and deleteSong only reloaded
playlists. Add refreshCurrentView() — drops the API cache and re-runs the active nav
entry's loader (playlists, playlist/album/artist detail, starred, home, …). deleteSong
calls it after the delete+rescan delay; deletePlaylist invalidates getPlaylists before
navigating. Bump SW cache to ns-v46.

Also record the post-1.2 work in the 1.2 changelog (importer Enter, search single-song
+ radio, NowPlaying order, official-audio artist-metadata match, JP-katakana album
artist fix, year back-fill, delete refresh).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Opus 4.8 committed 2026-07-26 23:27:29 +02:00
1 parent 966b318d9e
commit d98978a6c7
5 files changed
+36 -8

No files matched your search

+7
View File
@@ -61,16 +61,23 @@ Follow-up to 1.1: web history navigation, correct download tagging for mixed-art
### Added
- **Android** — Directional bottom-nav tab transitions: the slide follows the tapped tab's position (a tab to the right slides in from the right, one to the left from the left) instead of a fixed, sometimes-random-feeling direction. Applied across all four enter/exit/pop transitions; detail-screen push/pop is unchanged
- **Web** — Pressing **Enter** in the importer link field starts the import (same as the Importieren button)
### Fixed
- **Web** — Browser / mouse **Forward** navigation now works (previously only Back did): `popstate` applies the target history entry (`nsIdx`) for both directions, and the back handler no longer `pushState()`s over the browser's forward stack. In-app back buttons route through `history.back()` so the browser and app stacks stay in sync
- **Web** — Deleting a song or playlist refreshes the current view immediately (drops the cached list) instead of leaving the removed item visible for up to ~3 minutes
- **Web** — Album detail header showed the **first track's** artist (e.g. "TWICE" on a soundtrack) instead of the album artist; it now reads the album's own album-artist fields from `getAlbum` (matches the Android app)
- **Web** — Metadata autofill (iTunes / MusicBrainz) normalizes the trailing `" & "` in a multi-artist credit ("A, B & C" → "A; B; C"), but only when the value is already a list, so band names like "Simon & Garfunkel" stay intact
- **Android** — Erkunden (Apple) overlay back no longer *sometimes* jumped to Home: the root-level back callback is re-asserted on every navigation, so it can't lose back-priority to the NavHost after an album drill-down or tab switch
- **Backend (download)** — Mixed-artist albums search **per track**: the YTM/YouTube query and channel filter use each track's own performer, so soundtrack tracks by other artists (TWICE's "Strategy", MeloMance's "Love, Maybe") are found instead of failing with "Nicht eindeutig gefunden"
- **Backend (download)** — `album_artist` is derived from iTunes' album-level `collectionArtistName` (identical across the album) instead of the request's artist — which could be the lead single's performer, so a whole KPop soundtrack was tagged `album_artist = TWICE`. Applies to album and artist downloads; each track still gets its own per-track artist tag
- **Backend (download)** — Official audio is matched by yt-dlp's `artist` metadata, not just the channel name, so uploads on label channels (BANGTANTV, HYBE, "… - Topic") win over fan reuploads — e.g. BTS "Pied Piper" now grabs the official track. (`channel_is_verified` was rejected as a signal: lyric videos and fan MVs are also on verified channels)
- **Backend (download)** — Western artists are no longer tagged with the Japanese store's katakana album-artist ("DJスネイク" for DJ Snake): the JP-first iTunes lookup now keeps Japanese names only for Japanese songs and uses the US/English names otherwise (affects the `album_artist` tag and the on-disk folder)
- **Backend (download)** — Release year is back-filled from iTunes on single-track downloads where the source omitted it (the year tag was previously left empty)
### Changed
- **Web + Android** — Playing a song from search results queues only that song; Radio mode (if on) fills the rest — closer to Spotify. Album / playlist play still queues the whole list
- **Android** — NowPlaying track info is ordered title → album → artists, matching the web player
- **Web** — NowPlaying favorite star is vertically centered with the whole title / album / artist block instead of pinned to the title line
---
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'ns-v45';
const CACHE = 'ns-v46';
const PRECACHE = ['/', 'index.html', 'manifest.json', 'icon.svg'];
const SKIP_PATHS = ['/rest/stream', '/rest/download', '/config.js'];
// Auth token params stripped from cache keys so artwork survives across sessions/token refreshes.
+4 -6
View File
@@ -2,7 +2,7 @@ import { S } from '../state.js';
import { esc, formatDur, toast } from '../utils.js';
import { api, coverUrl, fetchRomajiIndex, romajiLookup, getRomajiOverrides, invalidateRomajiIndex, invalidateApiCache } from '../api.js';
// Circular — safe (only used inside function bodies)
import { loadPlaylists, loadPlaylistDetail } from '../views/views.js';
import { loadPlaylists, loadPlaylistDetail, refreshCurrentView } from '../views/views.js';
import { showConfirm } from './modals.js';
// ─── SONG INFO MODAL ───
@@ -312,11 +312,9 @@ export async function deleteSong() {
if (!r.ok) throw new Error(d.error || `HTTP ${r.status}`);
toast(`🗑 "${s.title}" gelöscht`);
closeInfoModal();
setTimeout(() => {
loadPlaylists();
if (document.getElementById('page-playlist-detail')?.classList.contains('active') && S._currentPlaylist)
loadPlaylistDetail(S._currentPlaylist);
}, 3000);
// Backend delete + Navidrome rescan takes a moment; then refresh whatever view is open (album,
// artist, playlist, starred, …) so the removed song disappears without a manual reload.
setTimeout(() => refreshCurrentView(), 3000);
} catch (e) {
status.textContent = '❌ Fehler: ' + e.message;
}
+2 -1
View File
@@ -1,6 +1,6 @@
import { S } from '../state.js';
import { esc, toast } from '../utils.js';
import { api, coverUrl, ndAuth } from '../api.js';
import { api, coverUrl, ndAuth, invalidateApiCache } from '../api.js';
// Circular — safe (only used inside function bodies)
import { playQueue, dlSongLocal, nativeInsertAt, renderQueue } from '../player/player.js';
import { navigate, loadPlaylists, loadPlaylistDetail } from '../views/views.js';
@@ -235,6 +235,7 @@ export async function deletePlaylist() {
await api('deletePlaylist', `&id=${pl.id}`);
closeEditPlModal();
toast(`🗑 "${pl.name}" gelöscht`);
invalidateApiCache('getPlaylists'); // drop the 3-min cache so the list reloads without the deleted playlist
navigate('playlists');
} catch(e) { toast('Fehler beim Löschen: ' + e.message); }
}
+22
View File
@@ -8,6 +8,7 @@ import { loadHomeData, loadAlbumRow, loadArtists, loadAlbums, loadGenreDetail }
import { loadArtistDetail, loadArtistAllSongs, loadAlbumDetail, loadItunesAlbumDetail, loadPartialAlbumDetail } from './detail.js';
import { loadPlaylists, loadPlaylistDetail, loadStarred, loadRandom } from './playlists.js';
import { loadNewReleases, loadRecommendations } from './discover.js';
import { invalidateApiCache } from '../api.js';
// ─── NAVIGATION HISTORY ───
export function _navSave() {
@@ -36,6 +37,27 @@ export function closeHeaderSearch() {
clearTimeout(S._headerSearchTimer);
}
// Force-refresh the active view after a mutating action (delete a song / playlist, etc.): drop the
// cached API responses so the reload is fresh, then re-run the current nav entry's loader.
export function refreshCurrentView() {
invalidateApiCache();
const entry = S._navStack[S._navCursor];
if (!entry) return;
const { page, data } = entry;
switch (page) {
case 'home': loadHomeData(true); break;
case 'albums': loadAlbums(); break;
case 'artists': loadArtists(); break;
case 'starred': loadStarred(); break;
case 'random': loadRandom(); break;
case 'playlists': loadPlaylists(); break;
case 'playlist-detail': if (data) loadPlaylistDetail(data); break;
case 'album-detail': if (data) loadAlbumDetail(data); break;
case 'artist-detail': if (data) loadArtistDetail(data); break;
case 'genre-detail': if (data) loadGenreDetail(data); break;
}
}
export function navigate(page, data=null) {
closeHeaderSearch();
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));