Code review of the Artist/ownership refactor surfaced three gaps:
- SyncUser updated User.Name on every login but never the linked
Artist row, so renames never reached track credits - defeating the
point of centralizing names in Artist.
- YouTubeTrackProvisioner looked up artists by ExternalId only when a
browseId was known, so it could never reuse the ExternalId-less
rows the migration backfilled from legacy Artist strings, creating
duplicate Artist rows for the same person. It now adopts the
backfilled row (assigning it the browseId) when one matches by
normalized name.
- AddArtistsAndTrackOwnership deduped legacy YouTube artist strings
case-sensitively before hashing them into a case-insensitive unique
index, and never guarded against local tracks with no UserHasTrack
row - either would abort the migration on real data. Backfill now
dedupes via DISTINCT ON over the normalized name and drops orphaned
local tracks before the ownership check constraint is added.
Track mixed catalog fields (Title, Artist credit, Duration) with
picture-processing and audio-transcode pipeline state as ten flat
columns plus two computed [NotMapped] flags, making the entity read
like a media-pipeline worksheet.
Introduce TrackPictures and TrackAudio as EF owned types mapped onto
the same existing columns (explicit column names, no schema change -
verified via `dotnet ef migrations has-pending-model-changes`).
Track now reads as Title/Artist/Duration/Pictures/Audio instead of
fourteen loose properties.
Drop the now-redundant Track.Artist string column now that every
track (local and YouTube) links its credited artists through
TrackArtist. TrackApplicationResponse formats the display string by
joining TrackArtists ordered by position, and every read query that
projects a track now includes TrackArtists/Artist so the join isn't
empty at response time.
Track.Artist was a plain string snapshot with no identity, and
UserHasTrack doubled as both "in my library" and "I own this",
conflating collection membership with administrative ownership.
Introduce Artist/TrackArtist to give artist credits their own
identity (needed so YouTube tracks with multiple credited artists,
and future renames, can be modeled), and Track.OwnerUserId to
represent ownership explicitly (local tracks only, enforced by a
check constraint). CreateTrack now provisions the uploader's Artist
row and links it via TrackArtist; DeleteTrack authorizes against
OwnerUserId instead of any UserHasTrack row.
Artist, ExternalId, OriginalPictureName and OriginalAudioName were
non-nullable with '' sentinels, which forced the unique (Source,
ExternalId) index to rely on a filtered "<> ''" clause. Make them
truly nullable and drop the empty-string sentinels, backfilling
existing rows to NULL. Also fixes a formatting regression
(`track != null&&`) introduced alongside the nullability change.
Playing a YouTube song now creates a lightweight track row (without queueing a download) and writes listening history; saving to a playlist queues the download via the new DownloadRequested flag.
Register AuditableEntityInterceptor as a singleton and attach it through the
AddDbContext options callback instead of newing it up inside
Database.OnConfiguring. The interceptor is stateless, so a single shared
instance is reused across contexts and its wiring lives in the DI layer.
The design-time factory keeps working (interceptor is runtime-only, irrelevant
for migrations).
Verified: build + ef migrations list (design-time context) OK; the Worker host
builds and validates the full DI graph (reaches endpoint configuration) with no
captive-dependency or interceptor error.
Build: 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds the PlayList picture-processing lifecycle saga (correlated by PlayListId)
mirroring the track one, and generalizes the failure plumbing:
- Rename the track fault translator to ProcessingSlipFaultConsumer and use a
generic Workflow.SubjectId variable + ProcessKind; it now emits typed
Track{Picture,Audio}/PlayListPicture ProcessingFailed events for all slips
- PlayListProcessingStateMachine (+ EF-persisted PlayListProcessingState,
migration): starts on CreatePlayListResourcesEvent, finalizes on
PlayListPictureProcessed, and on failure marks the playlist Failed and
removes the orphaned final picture (PlayListProcessingFailedConsumer +
MarkPlayListAsFailedActivity)
- UpdatePlayListPictureActivity publishes PlayListPictureProcessed on success
- Picture slip carries SubjectId + PlayListPicture kind + fault subscription
Verified end-to-end in Docker (RabbitMQ + migrated Postgres): both saga
endpoints, the fault translator and cleanup consumers configure, and an
addressing cross-check confirms all 21 activity execute queues the builders
address exist (caught + fixed a kebab mismatch on mark-play-list-as-failed).
Build: 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On definitive failure of the picture or audio slip, the saga now reaches a
Failed state and recovers end-to-end:
- Picture/audio track slips carry TrackId + ProcessKind variables and a
subscription to a fault translator (TrackProcessingSlipFaultConsumer) that
turns RoutingSlipFaulted into a typed Track{Picture,Audio}ProcessingFailed
- The saga stores the final object keys from the start event and, on failure,
transitions to Failed and publishes TrackProcessingFailed
- TrackProcessingFailedConsumer runs a cleanup slip that removes the orphaned
final picture/audio copies and marks the track LifeCycleStatus=Failed
(MarkTrackAsFailedActivity) — the track stays as a visible, recoverable record
- Migration for the new saga columns
Verified: Worker boots against RabbitMQ + Postgres in Docker; the new
endpoints (fault translator, failed consumer, mark-track-as-failed) configure
and the bus starts clean.
Build: 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Introduce a MassTransit saga state machine that correlates by TrackId and
tracks the two parallel processing slips (picture + audio):
- TrackProcessingStateMachine + EF-persisted TrackProcessingState
(pessimistic concurrency, EntityFrameworkRepository), migration added
- Starts on CreateTrackResourcesEvent; UpdateTrack{Picture,Audio}Activity now
publish TrackPictureProcessed / TrackAudioProcessed on success; when both
arrive the saga finalizes (Ready). Failure transitions to a Failed state
(cleanup wired in the next phase)
- Add LifeCycleStatus.Failed and the four outcome events
Verified: booted the Worker against RabbitMQ + migrated Postgres in Docker;
the saga endpoint 'track-processing-state' is configured and the bus starts
clean.
Build: 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move all .NET (6 projects, solution, Directory.*.props, nuget.config)
into backend/ so the monorepo root can host non-.NET projects later
- Remove throwaway test artifacts: player.html, pl.json
- Trim dead Flutter rules from .gitignore
Build from backend/Musify.slnx: 0 warnings, 0 errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>