* refactor(model): extract canonical 128-bit base62 id codec
* feat(model): generate random ids as canonical 128-bit base62 values
* feat(scanner): emit legacy PIDs in canonical base62 encoding
* feat(db): add id canonicalization transform for the uniform-ids migration
* feat(db): migrate all ids to canonical 128-bit base62 encoding
* fix(db): canonicalize ids in junction tables and JSON columns
* chore(jellyfin): update id-family notes for uniform canonical ids
* test(ids): harden codec input contract and migration edge coverage
* refactor(model): use log.Fatal for Encode128 contract guard per project convention
* fix(db): force full rescan after id migration for legacy PID configs
* test(db): guard id-column inventory against schema drift
* refactor(ids): compile-time Encode128 contract and unified column rewrite helper
* refactor(db): apply review feedback to id migration
Filter empty strings in collectColumn's SQL, reuse a prepared statement
for rewriteColumn updates, and clarify the legacy ID functions' comment
now that they emit the canonical encoding.
* feat(auth): split session and public-link JWT secrets, rotating sessions on id migration
* test(subsonic): initialize public token secret in helpers suite
The suite sets auth.TokenAuth directly instead of calling auth.Init, so the
new PublicTokenAuth was nil whenever Ginkgo's spec order ran a helpers spec
before any spec that calls auth.Init, panicking in publicurl.ImageURL.
* refactor(db): inline canonicalID into its only consumer, the uniform-ids migration
* refactor(model): rename Encode128/Decode128 to Encode/Decode
With every id now exactly 128 bits, the width suffix is redundant; the
package-qualified id.Encode/id.Decode carries the same information.
* test(db): make the id-columns guard classify JSON columns too
The guard only inspected columns named id/pid/*_id, so it could not see ids
embedded in JSON. Widen it to *_ids and to every JSON column, and drive the
"covered" set from a new embeddedIDColumns list instead of the inline calls
in the migration.
Every JSON column the schema has now carries a verdict. The four denormalized
caches -- media_file/album.participants, media_file/album.tags,
album.folder_ids and artist.similar_artists -- hold only artist, tag and
folder ids. Those all come from id.NewHash, whose 22-char base62 encoding of
a 128-bit MD5 is already in canonical range, so canonicalID is the identity
on them and the migration correctly leaves them alone. A new codec test pins
that invariant, since the exemptions depend on it.
Verified on a copy of a 727MB/96k-track production database: canonicalizing
those four columns changed zero rows, and artist, tag and folder ids were
themselves unchanged by the migration (only media_file ids moved, 95108 of
96666).
* fix(share): enforce track membership on public share streams
The public share stream endpoint (GET /share/s/{jwt}) validated that the
share existed, was unexpired, and that the share owner had library access
to the requested track, but it never verified that the track was actually
a member of the share. It also accepted stream tokens with no share id
(sid) claim, skipping share checks entirely.
Enforce that the requested media file belongs to share.Tracks, and make
the sid claim mandatory on the stream path. The only producer of stream
tokens (encodeMediafileShare) always sets sid, so no legitimate flow is
affected; the image endpoint decodes independently and is unchanged.
Also document why a JWT is used to represent a shared track: it is a
signed, scoped capability for a single public share, not part of
authentication.
* docs(share): clarify JWT usage comment wording
* fix(playlist): respect the user's library access when resolving M3U paths
FindByPaths looked up paths across all libraries, so importing an M3U
could add tracks from libraries the importing user has no access to.
Apply the user's library filter to the lookup, matching every other
media_file read. Admins and the (admin-context) scanner are unaffected.
* fix(share): scope shared playlist tracks to the owner's libraries
loadMedia loaded playlist tracks with a fake-admin context, so a shared
playlist could include tracks from libraries the owner has no access to.
Load as the share owner instead, so the library filter applies.
Admin-owned shares are unchanged.
* fix(share): only serve shared tracks the owner can access
A shared stream fetched the media file by id without checking the share
owner's library access, so it could serve tracks from libraries the
owner has no access to. Gate share-scoped streams on the owner's library
access. Non-share streams are unaffected.
* test(share): tidy library-access test setup
Consolidate the repeated share-owner test fixture in handleStream into a
helper, assert on track fields with HaveField instead of building an id
slice, and delete the scratch media file through the public repository
method.
* style: trim verbose comments in library-access checks
* fix(share): guard against nil owner and clean up test users
Add a nil check after loading the share owner so a missing user yields a
clear error instead of a possible nil dereference, and delete the users
created by the new tests in their AfterEach blocks.
* fix(share): avoid panic when a shared playlist is no longer visible to its owner
Tracks() returns nil when the playlist can't be loaded under the owner's
context (e.g. a public playlist shared by a non-owner that was later made
private). Capture the result and return early instead of chaining GetAll
on a nil repository, leaving the share with no tracks.
* fix(sharing): validate JWT expiration and share existence on stream endpoint
The public stream endpoint (/public/s/{token}) was using
TokenAuth.Decode() which only verifies the JWT signature but skips
exp claim validation. This allowed expired share stream URLs to remain
functional indefinitely. Additionally, deleting a share did not revoke
previously issued stream tokens since the handler never performed a
server-side share lookup.
Fixed by switching decodeStreamInfo() to use auth.Validate() which
properly checks the exp claim, and by embedding the share ID ("sid")
in stream tokens so the handler can verify the share still exists.
Old tokens without the sid claim remain backward compatible but still
benefit from expiration validation.
* fix(sharing): check share expiration on stream requests
Replace the lightweight Exists() check with Get() + expiration
validation, so that shares whose ExpiresAt was updated to an earlier
time after token issuance are also rejected (410 Gone). Reuses the
existing checkShareError handler for consistent error responses.