Albums previously had no cover concept at all: no picture upload
flow, no GET /albums/{id}/cover, and AlbumApplicationResponse's
image-key fields were plumbed but never populated. Description,
Pictures and TrackCount already lived on the shared Album base entity
for both local and external albums, so unifying the cover story only
required building the missing local-album picture pipeline.
Backend, cloning PlayList's existing async picture-processing
architecture exactly (MassTransit routing slip + saga), but making
the picture mandatory instead of optional (matching Track's pattern):
- AlbumPictures moves from all-nullable to required fields.
- New AlbumConfiguration (routes/sizes), deliberately without
PlayList's preset-picture fallback since a picture is now required
up front.
- CreateAlbum now takes a mandatory PictureIntentId, validates and
consumes the upload intent, and kicks off the same
copy-to-final -> resize -> upload -> update-entity routing slip
PlayLists use, via a new AlbumProcessingState saga that marks the
album Failed on a picture-processing failure.
- UpdateAlbum takes an optional NewPictureIntentId to replace the
cover later.
- New GET /albums/{id}/cover and POST /albums/upload-picture
endpoints, mirroring the playlist ones.
- New EF migration for the AlbumProcessingState saga table.
Frontend:
- AlbumForm gains a cover dropzone, required on create and optional
on edit (via ImageDropzone's new `required` prop), uploaded through
the same presigned-upload helper playlists use.
- New /api/albums/{id}/cover proxy route.
- PlaylistArt gains a generic coverUrl prop so AlbumHeader and the
album list/detail cards render the real uploaded cover instead of
only the track mosaic fallback.
Delete-time picture cleanup for albums is intentionally left as a
follow-up, matching the existing gap in playlist deletion.
New Playback:AllowAnonymousListening / Playback:AnonymousFragmentSeconds
settings control whether a request without a session can stream at all,
and if so, whether it's limited to a fragment instead of the full track:
- GET /tracks/{id}/stream and GET /youtube/tracks/{videoId}/stream no
longer require authorization outright; the handlers gate on the config
instead, so both keep working exactly as before when it's off (the
default).
- The fragment cap is enforced by the Streaming Gateway, not the client:
the stream ticket now carries an optional maxBytes claim (computed from
AnonymousFragmentSeconds and the transcoder's known bitrate), and
TicketValidationMiddleware clamps the Range it forwards to SeaweedFS to
stay under it — synthesizing one if the request didn't send one, and
answering 416 outright if the requested range starts past the cap.
- A YouTube track that hasn't been provisioned server-side yet streams
straight from YouTube's CDN, bypassing the Gateway entirely — there's no
way to cap that, so an anonymous request for one is refused (409) instead
of silently handing out an uncapped stream.
- ListeningHistory has a non-nullable UserId, so anonymous plays simply
don't get recorded rather than trying to force a row through the FK.
Also fixes a real, pre-existing bug this surfaced: the AppSettings.Development.json
of the Api, Worker, and StreamingGateway all pointed StreamTicket/Workers
paths at "../../deploy/..." — one directory short of deploy/'s actual
location at the repo root relative to each project's own folder. Docker
never hit this (it mounts an absolute /keys path), so it had apparently
never been exercised via `dotnet run` before.
Moves the six backend projects on disk into Core/ and Hosts/
subfolders, matching the solution folders already defined in
Musify.slnx, so the file explorer reflects the same layout as the
solution:
backend/Core/Musify.Domain
backend/Core/Musify.Application
backend/Core/Musify.Infrastructure
backend/Hosts/Musify.Api
backend/Hosts/Musify.Worker
backend/Hosts/Musify.StreamingGateway
Updates every reference to the old paths: ProjectReference entries in
Musify.Api.csproj and Musify.Worker.csproj, the three Dockerfiles,
deploy/scripts/migrate.ps1|sh, deploy/zitadel/provision.ps1,
deploy/up.ps1, deploy/README.md and docs/development.md. Also fixes a
stale Dockerfile comment pointing at a compose file that no longer
exists.
Verified with `dotnet build Musify.slnx` against the new layout.