iTunes ranks by popularity, so searching "WRLD Tunes" returned Juice WRLD
first. Now fetches 5 results and picks the first where all query words
appear in the iTunes artist name, preventing false-positive matches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Search iTunes with original Japanese names (accurate) rather than
pre-converting with kuromoji (inaccurate for uncommon names/readings).
iTunes returns the correct English artist name and title directly.
Also run iTunes enrichment for single-track Spotify imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add romajiForStorage() to romaji.js — smart word spacing + Title Case
- ytdlp.js: apply to folder names, filenames, and embedded tags
- novasonic-server.js: apply in edit-metadata endpoint and Spotify resolve
- spotify.js: update track artist/name from iTunes canonical English names
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Space as fallback separator on save when no explicit separator present
(Artist1 Artist2 → Artist1;Artist2)
- & and / added to explicit separator set everywhere
- navidromeArtist (download path) also handles /
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_fmtArtists and _normalizeArtists now split on & and / in addition to
• · ; , — so "Arriez & King Dif" typed in the field normalizes correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"Arriez & King Dif" was used as-is for the folder; now splits on , ; &
so only the first artist is used: "Arriez".
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getSong often omits albumArtist; getAlbum always returns album.artist.
Enrich the song object before showing the edit form.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Album name no longer shown inline next to the artist — navigate via
right-click > Zum Album instead, available everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Normalize •, ·, ;, , separators on save (both edit and download paths)
- Fix ndSong scope bug (was inaccessible outside try block in edit-metadata)
- Apply _normalizeArtists to artist/albumArtist before ffmpeg tags and file rename
- Storage format: Artist A;Artist B (no space, Navidrome/MusicBrainz style)
- Display format: Artist A; Artist B (space, via _fmtArtists in frontend)
- Show separator hint below label in metadata editor fields
- Remove hover effect on player bar song title
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Naming convention (download + edit-metadata):
Before: {01 - }Title.ext
After: {01 - }Artist1, Artist2 - Title.ext
- ytdlp.js: builds fileBase with artist prefix; findExistingTrack checks
both new and old format so existing files aren't re-downloaded
- metadata.js: findExistingTrack accepts optional safeArtist param
- novasonic-server.js: file rename on title edit now includes artist
(falls back to Navidrome's stored artist if not provided in body)
Song list subtitle: hide album name when album === title (singles where
the album is named after the track appear to duplicate the title)
Player bar: removed .player-title:hover underline/colour effect
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When the user opens the edit modal before the getSong async call returns,
the form fields are populated from the initial song object (from the song
list) which may omit albumArtist and other fields that only getSong
returns. Now, when getSong resolves and edit mode is already visible, all
form fields are updated to the authoritative Navidrome data.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After saving metadata with ffmpeg:
1. Title change → rename the mp3 in place to "{01 - }?{sanitized title}.ext"
(same convention as the downloader uses)
2. Album-artist or album change → compute new {artistFirstPart}/{album}
directory, move the file there (create dirs as needed)
3. Old album dir is deleted if empty after the move; old artist dir
is deleted too if it also becomes empty
When only the album name changes (no artist change) the old in-place
folder-rename behaviour is preserved.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
iTunes: split artist by [\s,;/] not just space so "VXLLAIN, iGRES"
yields first word "vxllain" (no trailing comma) and matches correctly.
MusicBrainz: extract first artist from the full credit string (split on
,;&•) and use `artistname` field — MB stores credits like "VXLLAIN feat.
iGRES & ENXK" which never matched the comma-joined full string.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Info modal view now shows Album-Künstler row from Navidrome albumArtist
- Song Importer detects youtube.com / youtu.be URLs (red badge)
- New /api/youtube/resolve endpoint: yt-dlp --dump-json for single
videos and playlists (up to 50 entries), enriched with iTunes lookup
for proper album/year/genre/cover on single tracks
- New src/youtube.js renders preview card and delegates download to
executeSpotifyDownload (which already handles sourceUrl → direct yt-dlp)
- Importer hint updated to list YouTube as a supported source
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an editable Album-Künstler input to the info modal. Populated from
Navidrome's albumArtist field on open; saved as album_artist tag via
ffmpeg. Explicit value takes precedence; falls back to deriving from
artist (first semicolon segment) when artist is changed without an
explicit albumArtist.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSON-LD could set track_name but return empty byArtist, making the OG
block (guarded by `if not track_name`) never run — leaving artist blank.
Now OG runs whenever either name or artist is missing, and only fills
fields that are still empty so JSON-LD name isn't overwritten.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backdrop click handler was too easy to trigger accidentally,
especially when editing fields. Close button handles dismissal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes LRCLIB lookup, .lrc sidecar file writing, and lyrics/uslt
metadata embedding from all downloads.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SC resolve: strip yt-dlp placeholder albums (starting with '['), use track title as album for singles
- edit-metadata: if Navidrome path stale, search music root recursively for file by name
- Both single and playlist SC track handling updated
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
JSON-LD byArtist gives the correct artist list. As secondary fallback,
OG desc format is 'Artist · Song · Year' not 'Song · Artist · Album',
so use desc_parts[0] not desc_parts[1] for artist.
Also handle both attribute orderings in meta tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SC tracks: use artworkUrl/album/genre fields (not iTunes artworkUrl100/collectionName/primaryGenreName)
- Playlist handler: same SC field fallbacks for artwork, album, genre, year
- ytdlp.js: convert & to , in album_artist tag so Navidrome albumartist.Split works
- Importer: clear both previews on any URL input change, not just on clear
- edit-metadata: rename album folder on disk when album name changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Missing imports caused Object.assign(window,...) to throw before navigate
was ever assigned, breaking all onclick nav buttons.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove top-level ui.js import from soundcloud.js (broke views.js→soundcloud.js→ui.js→views.js cycle)
- Use window.executeSpotifyDownload() instead to avoid the circular dep
- Merge Spotify + SoundCloud into one unified URL input that auto-detects
the service and colors the badge/button accordingly
- resolveImporterLink() routes to the correct handler based on URL
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New src/soundcloud.js: resolves SoundCloud URLs via yt-dlp --dump-single-json,
renders track/playlist preview with per-track download buttons
- Backend POST /api/soundcloud/resolve: extracts metadata (title, artist, genre,
artwork, duration) directly from SoundCloud — no iTunes lookup needed
- ytdlp.js: sourceUrl param — if set, downloads directly from URL instead of
ytsearch cascade (used for SoundCloud tracks)
- Spotify importer page renamed to Song Importer, now contains both
Spotify and SoundCloud sections in one view
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-map 0:v? copies the embedded cover stream from the original file when
no new artwork is supplied, preventing it from being silently stripped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Info modal gets an edit pencil button that switches to edit form
- Editable fields: title, artist, album, genre, year, track#, disc#, total tracks
- Cover art: upload your own image or auto-fetch from Cover Art Archive via MusicBrainz
- MusicBrainz button queries /api/musicbrainz/lookup and autofills all fields
- Save POSTs to /api/edit-metadata which rewrites tags via ffmpeg and triggers Navidrome rescan
- Backend: GET /api/musicbrainz/lookup + POST /api/edit-metadata endpoints
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
navidromeArtist() now writes ; instead of / so Navidrome splits
correctly with the updated Tags.Artist.Split config.
Also handles & (e.g. "Artist A & Artist B" → "Artist A;Artist B").
Add fix-artist-separators.sh — one-time Debian script to update
existing library files from " / " to ";" in artist tags.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extract parseArtists() helper — uses Navidrome's resolved artists array
when it has multiple entries, otherwise splits artist string on ';' only.
Removes old split on / that broke Au/Ra, HUNTR/X etc everywhere.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Manual split on / broke artist names like HUNTR/X and Au/Ra.
Now uses s.artists (OpenSubsonic array with correct IDs per artist)
and falls back to s.artist as a single unsplit link.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Page loads instantly with song-count order, then silently re-sorts
once the backend aggregates play counts across all songs. Result cached
20 min server-side to avoid repeated full-library fetches.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shuffle now shuffles the queue array once (current song first, rest
randomised behind it) instead of picking a random index on every skip.
Toggling shuffle off restores the original queue order. Removed the
'once' repeat state added in the previous commit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scrobble timer: 12s → 15s
- Play count moved after action buttons, shown as "23×" (distinct from duration)
- Song star/menu buttons always visible (removed opacity:0 hover-only rule)
- Artist all-songs page: added star + 3-dots to every row (same as top songs)
- Hero card: picks from full recent+frequent pool (was capped at first 5 = all frequent)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Scrobbling:
- Replace end-of-track scrobble with 12s timer approach
- Android: schedule on onTrackChanged, cancel on pause
- Web: schedule in playCurrent, cancel in togglePlay pause + onEnded
- Skip/new track resets the timer automatically (new song ID)
Artist top songs:
- Add star button and 3-dots context menu to every top song row
- Topsong grid expanded to 5 columns to fit action buttons
Settings — play count toggle:
- New "Bibliothek" section with toggle switch
- When on: shows play count in album/playlist song rows
- Artist page always shows play count regardless of toggle
- Persisted in ns_cfg localStorage
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Scrobble to Navidrome on auto-advance (Android) and track end (web)
so type=recent and type=frequent are accurate for the hero card
- Genre section: pill chips → grid cards with colored left bars
- Top 15 genres shown by default (sorted by song count), expand toggle
- Genre card: click → genre detail page with full song list
- Genre card: hover play button → shuffle-play without navigating
- Genre detail page reuses album-detail layout (back btn, stats, song list)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Home view:
- Add hero card (random pick from recent+frequent combined pool)
- Add genre browser chips (sorted by song count, tap = shuffle-play)
Android sync fix:
- Add currentId to getState() snapshot so JS can verify song identity
- onReady: reset stale localStorage queue when native song ID mismatches
- onTrackChanged: reject stale old-queue callbacks by ID verification
- onPlaybackStateChanged safety net: ID-guard prevents wrong-queue index corruption
- connectToService: call updateStateSnapshot() before onReady so getState() is never empty
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Dedup now checks only the last 50 queued songs instead of the entire
queue, so the radio doesn't stall on small libraries once every similar
song has been seen
- If every candidate is a recent duplicate (tiny library), allow up to 5
repeats rather than returning empty and letting the queue run dry
- Add second _queueGen guard after the getRandomSongs fallback await
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: addToQueue was sent to native twice for every radio top-up.
fillRadioQueue() already sends addToQueue internally, but onTrackChanged
and onQueueEnded also re-sent the same songs in their .then() callbacks,
causing native to accumulate duplicates. With native having double the
songs JS has, native indices no longer map to the correct JS queue entries.
Also fixed: playQueue called fillRadioQueue before setQueue was dispatched,
so addToQueue could land in the old native queue before setQueue wiped it.
Changes:
- fillRadioQueue: add _queueGen guard — bail if playQueue() replaced the
queue while the network request was in flight
- playQueue: increment _queueGen; move fillRadioQueue() inside the rAF
after setQueue so addToQueue always lands in the correct native queue
- onTrackChanged: remove .then() that re-sent addToQueue (double-send fix)
- onQueueEnded: remove addToQueue re-send, keep only _nsBridge('play')
- onPlaybackStateChanged: add safety-net sync when isPlaying transitions
to true — catches any remaining edge cases where onTrackChanged missed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The 120ms debounce on JS-initiated transitions (seek/setQueue) delayed
updateStateSnapshot(), leaving a stale window where getState() returned
the old index. If an AUTO callback fired during that window and the JS
onTrackChanged handler called getState(), it would read the stale index
and overwrite the correct player bar state with the wrong song.
Fix: always fire onTrackChanged immediately so updateStateSnapshot() runs
synchronously. The JS-side getState() reconciliation (from the previous
commit) then reliably gets the settled native index every time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two-pronged fix for the state drift between the system notification
(ground truth) and the WebView player bar:
JS (player.js): onTrackChanged now reconciles the incoming index with a
synchronous NSBridge.getState() call before applying it. Stale callbacks
from earlier rapid skips immediately correct themselves to the live
settled index instead of overwriting the correct state.
Kotlin (MusicService.kt): debounce JS-initiated transitions (seek /
setQueue) by 120 ms so rapid skip sequences produce one onTrackChanged
notification at the final index rather than one per skip step. Auto-advance
transitions are still fired immediately since JS doesn't initiate those.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces getRandomSongs with getSimilarSongs2 seeded from the current track.
Falls back to random if Navidrome returns nothing (no Last.fm config or unknown song).
Deduplicates against songs already in the queue.
Compatible with AudioMuse-AI plugin if added later — same endpoint, better results.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- MainActivity: emit onTrackChanged + openNowPlaying in one JS tick on notification tap
to prevent race where stale song appeared in NP overlay and bottom bar
- index.html: remove onclick/cursor from player-title so only album/artist are clickable
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
.player-status-row had min-height:16px + margin-top:3px = 19px of permanent
invisible space, making player-info ~63px tall on desktop. Centered in 112px
this pushed the title to ~25px from top instead of ~34px.
Switched badges from opacity:0 (still in layout) to display:none, removed
min-height — row collapses to 0 when no badge is active so player-info
is only as tall as the visible title+album+artist rows (~44px), and
align-items:center on player-track places them at the true bar midpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Desktop: player-track had align-self:center (from grid align-items:center),
collapsing it to 52px content height. align-items:center inside it then
centered against 52px, not the full 112px bar — text appeared at top.
align-self:stretch forces player-track to fill the bar height so internal
centering works correctly.
Mobile: padding:14px 12px was too symmetric — art filled the 46px content
area exactly and touched the progress strip. Changed to padding:6px 12px 14px
(6px top breathing room, 14px bottom for progress strip, ~54px content area).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
padding:0 12px 14px meant zero top padding, so align-items:center placed
content in the top 60px of the 74px bar — visually 7px above center.
Changed to padding:14px 12px (symmetric) so art+text center at exactly
37px, the visual midpoint of the bar. Progress strip stays absolute at bottom.
Artist color changed from hardcoded gold to var(--accent) in both the
bottom bar and Now Playing UI so it follows the active theme.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- line-height:1.2 on title and artist rows keeps the text block within
the 46px art circle so it no longer overflows above/below
- Artist name uses accent color (gold) in both the bottom bar and the
Now Playing UI, making it immediately distinguishable from the album
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Song restart: onPageFinished can fire multiple times mid-session (hash/state
changes in WebView). Added 3s debounce so onReady is not re-delivered
spuriously while a song is playing.
Notification artwork: warmArtworkCache was called from onTimelineChanged
where player.currentMediaItemIndex hadn't updated yet (lags until prepare()),
so it warmed the wrong tracks. WebBridge.setQueue now calls
MusicService.publicWarmArtwork(startIndex) immediately after setMediaItems
and before prepare()/play(), giving the bitmap loader a head start with
the correct index.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When onReady fires during MediaController reconnect (fragment navigation,
activity resume), calling setQueue restarts the current song from pos 0.
Guard: if native isPlaying=true, only re-sync JS UI — never touch the queue.
Only push a new queue when native is idle/empty (cold start).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Bottom bar: player-info is now a centered flex column so text block
aligns properly against the art circle
- Now playing: split artist·album line into album (above) + artist (below)
matching the bottom bar layout; removed np-album-sep reference
- Removed stale np-album-sep style.display call from player.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing the separator span from the HTML broke updatePlayerUI which still
called getElementById('player-album-sep').style — null crash on every
song play. Removed both occurrences since album now has its own line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
hideCtxMenu now guards against null #ctx-menu element. scroll/touchmove
listeners only fire when menu is actually visible, preventing spurious
calls on every scroll event.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Added _reorderCtx.active flag — plReorderEnter bails early if already
active, preventing duplicate handles and event listeners from stacking.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Player bar sub-line now shows album (clickable) on first row, artist below
- Context menu now closes on scroll and touchmove (was staying open on mobile)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
mousedown does not expose the input source — replaced with pointerdown
which carries pointerType='touch', allowing noTouch=true to reliably
skip touch input without affecting mouse/stylus on PC.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
noTouch=true now also adds a touchstart preventDefault listener so the
browser does not fire synthetic mousedown/click after a tap, which was
still triggering seeks in the PWA despite touch events being disabled.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a synchronous inline script that checks localStorage before first
paint and sets html.ns-authed, hiding #setup-screen via CSS immediately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Math.min(160) cap on 'lift' meant the overlay froze after 160px of drag
(at ~70% down the screen) while the finger kept moving — the felt 'stop'.
Now: player bar caps at its own height (once it slides off, no more movement
needed), overlay tracks the finger with no cap at 2.5× speed so ~360px of
upward drag fully reveals the now-playing screen. Release animation (from
the previous fix) then carries it smoothly to position 0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Swipe animation:
- _resetDrag(true) now reads the overlay's current drag position and starts
the CSS transition from there instead of resetting to translateY(100%) first.
That reset was causing the visible stutter (overlay jumped off-screen then
slid back up). Now the overlay continues seamlessly from wherever the finger
released to translateY(0).
Playing highlight:
- makeSongItem: adds data-song-id attribute to every .song-item element.
- updatePlayerUI: calls _updatePlayingHighlights() after every track change.
This toggles .playing on all visible song items in any open list/playlist/
favorites view so the highlighted row always matches what's actually playing,
without requiring the user to navigate away and back.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
-webkit-tap-highlight-color was only on 2 specific elements; every
div/song-item/card still showed a blue flash on tap in Android WebView.
*:focus{outline:none} replaces the button-only rule and covers clickable
divs as well.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- initProgressScrub: pass noTouch=true for the bottom mini-bar so touch
events are never registered on it; only mouse events remain (desktop-only).
The now-playing scrubber is unchanged and keeps full touch support.
- Export seekToMs(ms) from player.js; use it in the lyrics click handler
instead of S.audio.currentTime which has no effect when HAS_BRIDGE
(native ExoPlayer handles playback, not the HTML audio element).
- styles.css: add will-change:transform to .player and .np-overlay to keep
them on GPU compositor layers during the swipe gesture; tweak NP open
transition to cubic-bezier(.4,0,.2,1) for a snappier feel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- button:focus{outline:none} — removes the blue tap-highlight box in WebView
- Playlist reorder: replaced always-on drag handles with explicit 'Neu anordnen'
mode. Handles only appear after tapping the button; a sticky Speichern/Abbrechen
bar appears above the list. Touch drag is now fully supported. Context-menu
(long-press) is blocked while reorder mode is active to prevent conflicts.
- styles.css: .pl-drag-handle always visible when pl-draggable (no hover needed)
- Edit modal footer: wrap buttons on small screens so Save doesn't overflow
- Edit modal overlay: remove backdrop-filter:blur — causes WebView compositor
glitch where 3/4 of the modal disappears after returning from background
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Both 'Als Nächstes einfügen' and 'Am Ende der Queue' updated S.queue in JS
but never called NSBridge, so the native ExoPlayer ignored the change and
kept playing the original next item.
- Export nativeInsertAt(song, position) from player.js
- Call it in showCtxMenu with the exact splice index for insert-next
and -1 (append) for add-to-end
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fillRadioQueue: use addToQueue instead of setQueue so toggling radio mode
no longer resets position to 0 on the current song
- loadStarred: pass noArtistLink/noAlbumLink to prevent accidental navigation
taps on mobile (matches playlist behavior)
- styles.css: add pointer:coarse alongside hover:none so song-actions are
visible on Android WebView which may report hover:hover
- components.js: remove inline download button from song row; keep only star
and 3-dots (download stays accessible via context menu)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Single track and album downloads now navigate to the album page
- Artist downloads continue to navigate to the artist page
- Button label changes to 'Zum Album →' or 'Zum Künstler →' accordingly
- album name stored in job metadata so it survives until poll completes
- Search uses exact album+artist match, falls back to album-name-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- api.js: cache coverUrl() per id+size so WebView HTTP cache can work;
previously each call generated a new random salt/token → unique URL →
no cache hits → images re-fetched on every render
- player.js: replace _nsBridge('next')/'prev' with explicit seekToIndex
in nextSong/prevSong so native always jumps to the exact same index JS
computed; 'next'/'prev' relied on native's internal pointer being in sync
which caused 'UI shows new song but audio plays old one' when they drifted
- MusicService: set setSessionActivity PendingIntent on MediaSession so
tapping the playback notification opens the app
- MainActivity: handle open_now_playing extra in onCreate (cold start)
and onNewIntent (app already running) to show the Now Playing screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The backend uses NODE_PATH=/deps/node_modules with its own npm install
step — not the root package.json. Added kuromoji there and reverted
the incorrect root package.json change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- app.get/delete for /api/romaji-index were placed before const app = express()
causing ReferenceError on startup — moved into the ROUTES section
- kuromoji moved from backend/package.json to root package.json so Docker
picks it up during the existing npm install step
- Fixed dicPath resolution to use path.dirname(require.resolve(...))
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- Add /api/romaji-index endpoint using kuromoji morphological analyzer
- Fetches all Navidrome songs, converts kanji → hiragana readings → romaji
- Only indexes songs with Japanese characters; caches for 20 min
- /api/romaji-index DELETE clears cache (force rebuild)
Frontend:
- fetchRomajiIndex() in api.js: lazy-fetches and caches the index
- doLocalSearch: when query is romaji (ASCII), queries romaji index in
parallel with Navidrome search3 and merges+deduplicates results
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The magnifier icon in the top-right already opens search.
Having a second search box in the sidebar was redundant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Touch scrubbing now requires ≥8px horizontal drag before committing.
A plain tap on the progress bar is ignored — no unintentional position jump.
Also removed redundant onclick="seekTo()" since _initBarScrub already
handles mouse seeking via mousedown/mousemove/mouseup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When search input is pure ASCII (romaji), converts it to hiragana in
parallel using wanakana and merges both result sets. Deduplicates by ID
so no double entries. Helps find artists/songs tagged with hiragana in
sort fields (e.g. 'inabakumori' → 'いなばくもり').
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ⋮ button to every song-item triggering the context menu (same as right-click)
- Show song-actions always visible on touch devices via @media (hover:none)
- Search results: songs now render as makeSongItem rows instead of cards
(artists/albums remain as horizontal card rows)
- Fix Android app opening system settings on every launch after notification
permission denial — now only redirects to settings once using notif_settings_shown flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Each tryYtmSearch() call made a fresh HTTP request, so YouTube could
return different result orderings between steps. The correct video
(right title, wrong channel name) appeared in step 1 but was gone by
step 3 — a worse video with matching keywords slipped through instead.
Now fetch 10 YTM URLs once upfront and filter the same list with
progressively relaxed rules. Also saves 2-4 extra YTM requests per
download.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>