Commit Graph
30 Commits
Author SHA1 Message Date
Admin 6b69998865 feat: show explicit badge on YouTube Music results 2026-07-16 06:15:09 +02:00
Admin 6d73fe88ab feat(backend): record listening history for YouTube playback
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.
2026-07-16 01:15:36 +02:00
Admin 24595a17d6 feat(backend): auto-download YouTube tracks saved to playlists 2026-07-16 01:06:04 +02:00
Admin 2bd5ef729e feat(backend): YouTube Music search and playback resolution endpoints 2026-07-16 01:05:42 +02:00
Admin 87997d90a4 feat(backend): add source, external id and artist to tracks 2026-07-16 01:05:28 +02:00
Admin a59ce99051 style(backend): formatting pass on GetListeningHistory 2026-07-15 16:15:37 +02:00
Admin e5614178c8 fix(backend): stop loading a user's entire listening history into memory
GetListeningHistory used AsEnumerable() before DistinctBy/Take, which
forced EF Core to materialize every history row for the user just to
keep the last 15 distinct tracks. Now groups by track and takes the
15 most recently played track ids in SQL, then fetches only those.
2026-07-15 16:12:26 +02:00
Admin a1f7756fc9 fix(backend): compute track listens count via SQL instead of an unloaded navigation
TrackApplicationResponse.FromEntity counted track.ListeningHistories,
but none of the queries that built a response ever Included it, so
the collection was always empty and listensCount was always 0. Each
query now projects the count as a correlated subquery before mapping
to the response.
2026-07-15 16:09:54 +02:00
Admin 461bae1b01 feat(backend): add playlist cover endpoint and relax description validation
Adds GET /playlists/{id}/cover (mirrors the track cover endpoint) so
uploaded playlist pictures can actually be served. Also drops the
NotEmpty rule on playlist description, which rejected the blank
description the frontend treats as optional; CreatePlayList now falls
back to the domain's default text instead.
2026-07-15 16:09:40 +02:00
Admin 84ffb1eef6 Changes 2026-07-14 19:11:58 +02:00
Admin 2e866075a2 feat(backend): expose track duration and listen count in track response 2026-07-14 13:54:12 +02:00
Admin f6ac22af32 refactor(backend): return AudioTranscodeResult with status code and duration 2026-07-14 13:49:28 +02:00
Admin 209c02ded7 feat(backend): extract audio duration during transcoding and persist to track 2026-07-14 13:47:52 +02:00
Admin 78caf588be fix(backend): order listening history by recency and cap to 15 tracks 2026-07-14 13:26:04 +02:00
Admin 0b72363c33 feat(tracks): add track cover endpoint
Serve cover images via GET /tracks/{id}/cover with a size parameter, streamed from storage with long-lived immutable cache headers.
2026-07-14 04:54:40 +02:00
Admin a40f3da247 feat(tracks): add user listening history
Record a listening-history entry when a stream ticket is issued and expose GET /users/{id}/listening-history to read it back.
2026-07-14 04:54:35 +02:00
Admin 812913156b refactor(application): drop redundant default values on query records 2026-07-06 02:23:35 +02:00
Admin 927f965023 fix(playlists): reject empty/whitespace name and description on update 2026-07-06 02:20:47 +02:00
Admin 7623b4e80e refactor(search): treat empty search strings as no filter (string.IsNullOrEmpty) 2026-07-06 02:12:58 +02:00
Admin ac1c515dec feat(tracks): support search by name in GetTracks 2026-07-06 02:04:01 +02:00
Admin d6c269d80f Formatting 2026-07-05 16:04:53 +02:00
AdminandClaude Opus 4.8 a5305b8ed8 feat(infra): playlist processing saga + generalize fault handling (phase 3)
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>
2026-07-05 14:55:40 +02:00
AdminandClaude Opus 4.8 8664bfb800 feat(infra): track saga failure handling + cleanup (phase 2)
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>
2026-07-05 14:49:33 +02:00
AdminandClaude Opus 4.8 986941166c feat(infra): add Track processing saga (state machine, phase 1)
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>
2026-07-05 14:41:19 +02:00
AdminandClaude Opus 4.8 d08d82a483 fix(infra): repair TransferFiles compensation + tidy MassTransit consistency
Fix (recoverability bug):
- TransferFilesToBucketActivity returned Completed() with no log, so its
  compensation could never delete the transferred files (orphaned objects on
  fault). IStorageService.TransferFilesAsync now returns the real uploaded
  keys; the activity logs them so Compensate can remove them. Also drops the
  Path.Combine key recomputation (produced backslash keys on Windows).

Consistency / DRY:
- Normalize all courier activities to internal + folder-based namespaces
  (several were public and in the flat .Activities namespace)
- Extract the duplicated exponential retry into UseStandardRetry
- Unify endpoint-name logic into a single EndpointHelper (remove the parallel
  MassTransitEndpointNames helper)

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:58:27 +02:00
AdminandClaude Opus 4.8 423e06baf9 refactor(application): fold Pagination into Shared, rename assembly marker
- Move PaginatedResponse from the single-file Pagination/ folder into Shared/
  (namespace Musify.Application.Pagination -> Musify.Application.Shared)
- Rename MusifyApplication.cs -> AssemblyInfo.cs (it only holds the
  [assembly: MediatorOptions] attribute)

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:38:12 +02:00
AdminandClaude Opus 4.8 38f7094fb0 refactor(di): add AddApplication entry point and generic AddValidatedOptions
- Add Musify.Application.AddApplication(services, config) as the layer's DI
  entry point: registers Mediator, UploadIntentValidator and all app configs
- Collapse the 5 near-identical AddXConfiguration methods into a single
  generic AddValidatedOptions<T>(config, section)
- Move UploadIntentValidator registration out of Infrastructure into the
  Application layer (fixes layering leak); Infra config binding now uses the
  generic helper
- Api calls AddApplication; Worker uses AddValidatedOptions for its configs

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:36:46 +02:00
AdminandClaude Opus 4.8 e802c8565d refactor(application): co-locate each command/query with its handler
One file per use case under the feature folder (Tracks/PlayLists/Users),
dropping the Commands/Queries/Handlers sub-folders and their namespaces.
Each file now holds the record and its handler in namespace
Musify.Application.<Feature>; external usings updated accordingly.

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:12:59 +02:00
AdminandClaude Opus 4.8 6e469dbb4a refactor: replace Ardalis.Result with ErrorOr across the app
Ardalis Result<T> carried incoherent success sub-statuses and a clunky
generic converter; ErrorOr models success-or-errors cleanly.

- Application: all commands/queries/handlers now return ErrorOr<T>
  (non-generic Result -> ErrorOr<Success>); errors via Error.NotFound/
  Conflict/Validation/Unauthorized/Failure; success via value or new Success()
- Drop ResultExtensions.As<>(): error propagation is now just return .Errors
- Rewrite HTTP mapping (ErrorOrHttpExtensions) off Error.Type; success maps
  to 200 (value) or 204 (Success); Created still built in the endpoints
- Endpoints/gateway middleware: !IsSuccess -> IsError
- Swap Ardalis.Result package for ErrorOr 2.1.1 (remove unused ref from Worker)

Build: 0 warnings, 0 errors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 04:05:43 +02:00
AdminandClaude Opus 4.8 8f34ed4874 chore: group .NET into backend/ and drop non-.NET test artifacts
- 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>
2026-07-05 03:43:52 +02:00