users.api_token held each user's apiKey in plaintext and was looked up by
equality, so a leaked row or backup handed out working keys. Migration
0063 replaces it with api_token_hash (sha256, hex), computed in place
from the existing keys so every Subsonic client keeps working.
The key can no longer be read back: GET /api/me/api-token is gone, and
POST returns the new key once. Settings shows it right after Regenerate
with a copy button and a "won't be shown again" note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
d7a8e5f3 added a dataDir parameter to NewService and updated the 13
call sites spelled NewService(pool, lidarrconfig.New(pool), nil). Four
more build their client from a Lidarr stub, NewService(pool, cfg,
clientFn), and were missed, so the package's tests did not compile and
run 6495 failed both go vet and the integration build.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
Two delete paths had opposite failure policies. tracks.RemoveTrack
logged a failed os.Remove and deleted the row anyway, which CASCADEs
likes, plays, playlist memberships and tags, while the file survived
for the next scan to re-import as a stranger. library.DeleteTrackFile
stopped correctly but reported it as a bare 500 nobody could read.
One path now: library.DeleteTrackFile removes the file first and, on
anything but ErrNotExist, returns *FileRemoveError with nothing
deleted. Only then does it delete the row and tidy an emptied album
and artist in one transaction, log the sync change and clear orphaned
artist art. RemoveTrack calls it, which also fixes RemoveTrack never
logging a sync change. Quarantine Delete file now tidies emptied
albums and artists too.
Both endpoints answer an unwritable library (EROFS, EACCES, EPERM) with
409 library_not_writable. The message names the directory (removal
writes to the parent), the uid:gid the server runs as, and that
nothing was deleted. Other remove errors are 500 file_delete_failed
with the path.
The reachable surface is quarantine Delete file, which failed
silently: no copy for the code on either client, and Android swallowed
the exception so the row just reappeared. Web and Android now have
copy for both codes and append the server message for exactly those
two. Android's quarantine screen shows it in a snackbar.
DELETE /api/admin/tracks/{id} has had no client since f7278f24, which
kept it on purpose for a safer admin surface, so its history loss was
latent. Fixed rather than removed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
The audit log treats lidarr_album_mbid as a 'this row triggered Lidarr'
marker. Setting it on Resolve/DeleteFile (where Lidarr is never contacted)
muddied that semantic. Reverting to plan: only DeleteViaLidarr writes the
mbid. Adds an idempotency test for Resolve over a track with zero rows
(audit row written, affected_users=0, mbid nil).
- Flag now detects Postgres SQLSTATE 23503 (foreign_key_violation) on
UpsertQuarantine and surfaces ErrTrackNotFound instead of a wrapped
generic. T8's handler can now return 404 track_not_found cleanly.
Constraint-name guard ('track' substring) keeps a future user_id FK from
mis-mapping to the same error.
- AdminQueueRow.AlbumTitle is now string (not *string). albums.title is
NOT NULL upstream; the empty-string-to-nil dance was answering a
nullability question that doesn't exist in the schema.
- Add TestFlag_NonexistentTrackReturnsErrTrackNotFound covering the new
branch.