Author SHA1 Message Date
Admin 63018a7c72 Move shared infrastructure to a separate Infrastructure repo
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger no longer run as part
of this stack. They're shared services (used by Musify, Ping and
docker-manager) that now live in a separate Infrastructure repository
and must already be running.

deploy/compose.yml keeps only what's specific to Musify: its own
database connection, its own OIDC app and storage bucket (created
against the shared Zitadel/SeaweedFS via zitadel-init/seaweedfs-init),
and the application containers. A new wait-for-postgres one-shot
replaces the in-file Postgres health check, which no longer works now
that Postgres lives in a different compose project.

compose.prod.yml joins Infrastructure's infra-net network instead of
running its own copies. Docs and READMEs updated to match.
2026-09-21 22:09:08 +02:00
Admin c5748e9765 Fix route 2026-09-21 22:08:53 +02:00
Admin 23e30d96fc Translate docs to English, rewrite README, drop design handoff folder
Translate all of docs/ from Spanish to English and rewrite the root
README as a short project overview instead of a technical run guide.
Remove web-player/design_handoff_musify_player, an old design-tool
export that was no longer needed.
2026-09-21 18:41:04 +02:00
NotS1mp 3f8d517ee3 Merge pull request 'Design Prot Merge' (#1) from design-prot into master
Reviewed-on: #1
2026-09-21 15:48:16 +00:00
Admin 2662abe460 Remove comments from backend and web-player source
Strip //, /* */ and XML doc comments from all backend .cs files, and
JS/TS/CSS/HTML comments from web-player source (.ts, .svelte, .css),
per the project's no-comments convention.
2026-09-21 17:12:18 +02:00
Admin 1b6d7ecb42 web-player: final sweep and guardrails
Add scripts/check-tokens.mjs (npm run lint:tokens, wired into lint) as
a guardrail: fails if src/**/*.svelte reintroduces arbitrary values
outside the B.4 allowlist, factory rounded-*, h-N w-N pairs (should
be size-N), quarter-step fractional spacing, or literal hex/rgba()
colors.

Cleans up the repo until the script reports 0 violations: ~40 h-N w-N
pairs -> size-N (mostly icons, via a one-off script), ~26 quarter-step
fractional spacings rounded to the nearest integer (delta <=1px, e.g.
py-2.75->py-3, gap-1.75->gap-2), stray factory radii (rounded-sm/lg,
bare rounded) migrated to the matching tokens, and two
tracking-[-0.02em] that matched the existing tracking-display token
exactly but had never been migrated. Removes the
stroke-thin/regular/bold recipes (added earlier, zero consumers) —
decision made, not blocking: strokeWidth={n} (13 values, across
almost every component) isn't migrated to CSS because there's no way
to verify without Docker that lucide-svelte honors stroke-width by
class instead of the prop, and a regression there would be silent
across the whole app; the script documents this gap explicitly
instead of pretending full coverage.

Orphan detector: 0 components with no importers. Removes
docs/design-system-refactor-plan.md (earlier plan, superseded by
frontend-refactor-plan.md). web-player/CLAUDE.md gains a "Styles"
section with the B.4 allowlist and the base components to reuse.
Recalculates the baseline table (A.4) and logs it: 61->47 components,
44->11 arbitrary values (the remaining 11 on the allowlist), 13->8
distinct radii, 0 lint:tokens violations.

Verified with npm run check (0 errors), npm run lint:tokens (0
violations) and npm run build. Smoke-tested /login with no console
errors attributable to this change.
2026-09-21 15:05:07 +02:00
Admin b60a87adb5 web-player: big pages (Explore, Home) and tracks.ts cleanup
This pass also tackles the task deferred earlier: removing
tracks.ts's TrackTarget wrapper and its 8 accessors
(targetId/targetTitle/targetArtist/targetOwnerUserId/targetExplicit/
targetListensCount/isTargetCurrent/queueItemForTarget/
targetForQueueItem). Explore, Home, Mix, menus.svelte.ts and mixes.ts
now use track objects directly, with
toQueueItem/trackFromQueueItem (new, in player.svelte.ts) as the only
converters. Along the way, TypeScript revealed that isExplicit
doesn't exist in the real /tracks schema: Explore's "explicit" badge
was already a no-op before this refactor (always undefined ??
false); the binding is removed instead of pretending support that
never existed.

Explore: GENRE_INFO/genreTiles are extracted to src/lib/genres.ts;
the search logic
(showGroup/capped/matchPlaylists/searchCounts/searchChips/findTopResult)
is extracted to src/lib/search.ts as pure functions parameterized by
filter/query instead of closing over component state. "Top result"
unifies the duplicated button/link with <svelte:element>. Decision
made without blocking: the 4 group blocks (Tracks/Albums/Users/
Playlists) are not collapsed into a single {#each groups} — each
group has genuinely different snippets, context menu and ListRow
props, and a generic config would have added more indirection than it
removed.

Home: uses Page instead of a hand-rolled page-x; the 3 stats become
an {#each}; the hero/spotlight backgrounds (inline style= with CSS
variables) become the hero-surface/spotlight-surface utilities, and
gap-11.5 is replaced by the --spacing-section token (2.875rem) via
gap-section.

Verified with npm run check (0 errors) and npm run build. /explore
without a session redirects to /login because +layout.server.ts
requires allowAnonymousListening from the backend (unreachable
without Docker this session) — pre-existing behavior, not a
regression. No authenticated visual QA; smoke-tested /login with no
console errors.
2026-09-21 14:51:43 +02:00
Admin 5fa901c2e6 web-player: app chrome (layout, navigation, player)
PlayerExtras is merged into PlayerBar, which now renders volume and
the queue button inline and uses the new Slider (a real range input
with thumb thickness controlled via CSS variables) for both the
progress bar and volume, replacing SeekBar and VolumeControl (grep
confirmed SeekBar's `compact` mode was dead code).

Sidebar and TabsBar migrate to appNavLinks(user, counts?), removing
the duplicated nav links between the two. Decision made without
blocking: Sidebar gains the gradient Logo mark it didn't have before,
for visual consistency with MobileHeader and the login screen.

PlayerDock and MobileHeader's z-index is fixed to z-(--z-sticky)
(they're fixed bars, not menus), and SegmentedControl's to
z-(--z-raised), per the plan's note B.1.5. glass-bar and
theme-transition are applied to PlayerBar (both variants), TabsBar,
Queue and Home (hero/spotlight), replacing duplicated
blur/transition styles.

Verified with npm run check (0 errors) and npm run build; smoke-tested
/login with no console errors. No authenticated visual QA (Docker
unavailable this session).
2026-09-21 14:39:20 +02:00
Admin 8975e43729 web-player: forms (CoverForm, SegmentedControl, Chip)
CoverForm.svelte is the shared skeleton (form + enhance + cover +
error message + buttons) for AlbumForm and PlaylistForm, which now
only keep their own fields via the `fields` snippet (next to the
cover) and `extra` (full width — not literally in the spec, but
needed: the album description and the playlist visibility picker
don't fit in the column next to the art).

ImageDropzone gains `size="hero"` (size-cover-hero + rounded-art-lg)
and an `onselect` callback; the upload-track page now uses this
component instead of reimplementing the cover picker by hand (wrapped
in {#key} so it can reset after publishing). Its empty-state icon
goes from 64px to icon-xl (36px), as the plan recommends.

SegmentedControl.svelte (generic, N options) replaces PlaylistForm's
Private/Public picker. Chip.svelte replaces Explore's filter chips
(selected/count) and the suggested-genre chips in "No results" (href,
outline style).

@utility field-control in layout.css unifies the repeated class
string shared by Input and Textarea.

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-21 00:58:52 +02:00
Admin 1bdd7ec9f1 web-player: headers, dialogs and typography
PageHeader absorbs CollectionHeader
(title/eyebrow/description/meta/cover?/actions?/align?): without a
cover it behaves like the old simple PageHeader (inline actions),
with a cover like the old CollectionHeader (actions below). It sets
its own bottom margin (mb-7/8); the mt-* each page used to add
afterward are removed. Migrates the 9 places that used either one
(albums, library, playlists, upload, Explore x2, liked, mix,
profile). CollectionHeader is removed.

AlbumHeader and PlaylistHeader are inlined into their pages and
removed. Decision (recommended, not blocking): PlaylistHeader gains
icons on Edit/Delete/Play, matching what AlbumHeader already had.

New ConfirmDialog replaces the two delete dialogs (album and
playlist), which had different typography. Modal now uses
.glass-panel (instead of duplicating the inline style=) and
z-(--z-modal); its eyebrow and title use the
text-eyebrow/text-display-3 recipes.

SectionHeading gains `count?` (a "N results" line, replacing 4 copies
in Explore) and `href?`/`linkLabel?` ("See all", replacing Home's).
Applies typography recipes (text-display-2/3, text-eyebrow,
text-body) and width tokens (max-w-prose-sm, max-w-hero) across
EmptyState, +error, "No results", and the Home/Queue/Sidebar hero and
stats. The "You haven't listened to anything yet" empty state
(duplicated twice on Home) becomes a local snippet.

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-21 00:48:23 +02:00
Admin 964dfc809f web-player: cards and grids (MediaCard)
MediaCard now builds its own art (src/trackIds via Artwork
size="fill", or a custom art snippet) and its own play button
(onPlay, with a scrim and a glass PlayButton on hover) instead of
relying on each wrapper passing it a snippet. Replaces AlbumCard,
PlaylistCard and MixTile, which are removed along with
MediaGrid.svelte (their grids move to the
grid-cards/grid-cards-lg/grid-tiles/grid-wide @utility recipes).

format.ts gains albumMeta() and playlistMeta() (previously ad-hoc
calculations inside each card). Explore's genre tile moves from an
inline style= gradient to the genre-tile @utility recipe +
--tile-hue. Along the way, fixes the "follower/s" pluralization on
the user profile that had been missed.

Delta documented: the art's hover-zoom and the scrim+PlayButton
(previously exclusive to MixTile) become MediaCard behavior gated on
the onPlay prop — they'll apply to any future card that uses it, for
consistency.

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-21 00:30:30 +02:00
Admin 8cea298b2a web-player: track tables (TrackList)
Add TrackList.svelte, replacing TrackTable+TrackRow+TrackIndexCell+
TrackMeta and the 6 track tables (library, liked, mix, album,
playlist, album's library picker): declarative columns
(added/uploaded/plays) with fixed widths as module constants, an
index with an EQ indicator for the active track (or an overlay on the
art when index={false}, for the library picker), and a rowAction that
builds the form+IconButton (removeTrack/addTrack/deleteTrack) or a
direct onclick (Like).

AlbumTrackTable, PlaylistTrackTable and LibraryPicker are inlined
into their pages and removed, along with the 4 components TrackList
replaces.

Decision (recommended by the plan itself, not blocking): remove the
"Album" column, which in all 3 tables that had it always showed "—"
(a placeholder with no real data).

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-21 00:21:15 +02:00
Admin fb4a8ccc0e web-player: rows (ListRow, MediaIdentity, EqBars)
Add MediaIdentity.svelte (renames and generalizes TrackTitleCell,
purely presentational: art + title + subtitle, no click handling)
and ListRow.svelte on top of it (href|onclick, active, size xs/sm/lg,
variant row|card, leading/trailing/overlay). Also used by
player/TrackInfo... (TrackInfo itself is left for a later pass, which
already has an explicit task to touch that file).

Migrated to ListRow: SearchResultRow (Explore, 4 groups — component
removed), Home (Continue listening, Popular, spotlight, Your
playlists), Queue (Up next), Sidebar (playlists). The 6 track tables
(AlbumTrackTable, PlaylistTrackTable, library, liked, mixes,
LibraryPicker) migrate to MediaIdentity with their own local
use:pressable, since TrackList doesn't exist yet.

EqBars gains an `overlay` prop (scrim/45 background, on-art color)
replacing NowPlaying.svelte (removed). Along the way, fixes 6
plural() calls that a previous pass had left unmigrated (liked,
library, mixes, playlists, PlaylistHeader, Home).

Deltas documented in the plan's log: Sidebar's active row now uses
the accent tint (previously neutral gray); Home's "Your playlists"
card loses its redundant author line and its radius becomes
rounded-art; Queue rows lose the art's opacity-80.

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-21 00:14:20 +02:00
Admin 14ee1dc45d web-player: unify the context menu (ContextMenu) + AccountMenu
Add src/lib/components/ui/ContextMenu.svelte, replacing
TrackContextMenu and AlbumContextMenu (109 and 119 lines, 90%
identical): same positioning function (contextMenuPosition, exported
from the module, using MENU_WIDTH/SUBMENU_WIDTH moved to config.ts),
same backdrop, same Escape handling, same generic "Add to playlist"
submenu via {items, playlistAction, playlists}. Uses MenuItem for
actions (previously each reimplemented its own classes by hand) and
Artwork size="xs" for playlist thumbnails.

menus.svelte.ts (createTrackMenu) adapts to the new
contextMenuPosition; Home, Explore (track and album menus) and Mix
are migrated.

AccountMenu: panelClass (free-form Tailwind classes) becomes prop
width: 'sm'|'md'; adds Escape-to-close (accessibility improvement, it
didn't close before); TopBar and MobileHeader no longer wrap it in a
duplicate div.relative (its own root already is one). The panel's
z-index becomes z-(--z-menu), matching ContextMenu (previously z-20
in TopBar / no explicit z in MobileHeader).

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-20 23:05:53 +02:00
Admin 1320b12699 web-player: TypeScript helpers (partial)
Add src/lib/actions/pressable.ts and clickOutside.ts (Svelte actions;
role/tabindex stay static in markup for SSR accessibility, the action
only adds click + Enter/Space) and src/lib/toggle.svelte.ts
(createToggle), used by playlists.svelte.ts and queuePanel.svelte.ts.

format.ts: adds plural(n, one, many); migrates the repo's 15 manual
pluralizations, unifying them to lowercase (visible diff in
Playlists, Home, liked, Explore).

player.svelte.ts: adds toQueueItem, isQueueCurrent, playAllOrToggle,
playShuffled; renames addToQueue -> playNextItem (the old name was
misleading: it inserts next, not at the end). Migrates the 5 places
that repeated the isCurrentQueue/playAll pattern (album, playlist,
liked, mix, Home). Simplifies liked.svelte.ts and recentlyPlayed.ts
with object spread instead of rebuilding every field by hand.

navigation.svelte.ts: adds searchHref (migrates the duplicated
buildHref from TopBar and Explore) and appNavLinks (added; its
adoption in Sidebar/TabsBar is left for a later pass, as noted
there).

menus.svelte.ts: createTrackMenu() replaces the manual context-menu
state (contextMenu + contextMenuStateFor + addToQueue) in Home,
Explore and Mix.

Deliberately left pending: removing tracks.ts's TrackTarget wrapper
and its 8 accessors is deferred — it's the riskiest change in the
plan, touches types across Home/Explore/Mix/menus, and couldn't be
visually verified without Docker this session.

Verified with npm run check/build.
2026-09-20 23:01:13 +02:00
Admin aa20e70671 web-player: buttons (Button, IconButton, PlayButton)
Button: subtle variants (primary/secondary, left unchanged per the
user's call), glass (formerly secondaryOnGlow, with its own
font-normal), accent, strong (cta-strong/ink, replacing the `!`
overrides on the Home hero), danger. Uses the --color-btn* tokens
instead of bg-[var(--mf-btn-...)]. Radius unified to rounded-control.

IconButton: adds shape (square/round), size (xs/sm/md/lg), tone
(muted/subtle/plain), pressed (aria-pressed + text-accent),
href/reload (links), surface (permanent background + hover to
surface-hover instead of the generic `hover` alias, which matched
surface-2 and gave no contrast).

New PlayButton: a single place for the play/pause SVGs, strong/glass
variants, as="span" for non-interactive overlays. Replaces
TransportControls' main button, the MixTile overlay, and the "Top
result" circle in Explore.

Migrated to IconButton: close queue, "Create playlist" (Sidebar),
Like (TrackInfo, with pressed), queue (PlayerExtras), back (TopBar),
login (MobileHeader), and the row actions in tables (remove/add, size
xs). BackLink uses size-icon-sm and drops a duplicate hover.

Decision (not blocking): remove TopBar's decorative "forward" button
and PlayerExtras' decorative AlignJustify icon (visually dead code).
TransportControls' buttons (shuffle/prev/next/repeat) are
deliberately left unmigrated: they currently have no background or
hit-box of their own, and IconButton always applies a fixed-size
box — migrating them would have added a visible hover pill that
couldn't be verified without Docker this session.

Verified with npm run check/build.
2026-09-20 22:48:08 +02:00
Admin a4ca1f32d3 Remove no-op playlist picture migration; cover picture-add transition in tests
The Sept migration MakePlayListPictureColumnsOptional altered the
PlayLists picture columns to nullable:true when they were already
nullable:true (set back in MakePlayListPicturesNullable, two months
earlier) — a dead migration left over from making the Pictures
navigation itself optional, which produces no actual schema change.
Verified with `dotnet ef migrations has-pending-model-changes` and a
full migration run against a fresh Postgres container that removing
it introduces no drift.

Also add a test for updating a playlist that has no picture yet,
which was the one Pictures-related state transition without coverage.
2026-09-20 22:40:40 +02:00
Admin 58eb670fed web-player: unify Artwork and Avatar
Add src/lib/components/ui/Artwork.svelte and Avatar.svelte (B.3) and
migrate every use of Cover, PlaylistArt, MixArt and ArtistAvatar
(Sidebar, Queue, TrackInfo, TrackContextMenu, AlbumContextMenu,
TrackTitleCell, AlbumCard, PlaylistCard, MixTile, AlbumHeader,
PlaylistHeader, Home, Explore, mix, user profile, TopBar,
MobileHeader). Remove the 4 old components.

Artwork: size decides both dimension and radius automatically
(cover-*/radius tokens from the earlier token pass); fallback chain
is src -> mosaic (>=4 trackIds) -> first cover -> placeholder,
cascading if each step fails (previously split across 3 components
with slightly different logic each). Adds a 'fill' size (aspect-square
w-full), not in the spec, needed for fluid grid cards until MediaCard
lands later.

TrackTitleCell's `coverSize` prop (free-form Tailwind classes)
becomes `size: 'sm'|'lg'`, delegating to Artwork. SearchResultRow
loses its `kind` prop and the per-type radius (now decided by
Artwork/Avatar).

Product decision (not blocking): remove the name under the profile
avatar on /u/[id] since it duplicated the CollectionHeader title.

Small radius/size deltas are documented in the plan's README
(auto-radius by size replaces ad-hoc classes; see table B.1.1/B.1.2).

Verified with npm run check/build; no authenticated visual QA (Docker
unavailable this session).
2026-09-20 22:39:05 +02:00
Admin 7b1b81c45a web-player: single source of truth for theme, no light-mode flash
Remove the duplication of the ~18 per-hue dynamic tokens between
theme.css and buildThemeTokens(): theme.css now only defines fixed
neutrals, surfaces, layout and z-index; hue/mode-dependent values
come solely from tokens.ts, injected via SSR as an inline <style> in
+layout.svelte (tokensToCss helper) and reapplied on every hue
change. app.html sets data-theme before first paint by reading
localStorage, removing the dark flash when light mode was saved.

The two inline-style backgrounds become @utility
app-backdrop/app-vignette; the main's bottom padding becomes
@utility app-main, driven by --mf-dock-h via [data-has-track].

Note: <style>{expr}</style> doesn't interpolate in Svelte (it
compiles as component-scoped CSS) — the SSR block is injected with
{@html `<style>${css}</style>`} since the content is fully generated
(no user input).

Verified in the browser: --mf-accent/--mf-bg resolve correctly during
SSR, data-theme='light' applies before the first render with no
flash.
2026-09-20 22:26:37 +02:00
Admin ea9bf07126 Clean up Application layer commands/queries readability and correctness
Add missing AsNoTracking() on read-only queries, drop redundant
Update() calls on already-tracked entities, and fix inconsistent
formatting (stray spaces in generics, ternaries, and lambdas) across
the Albums/Tracks/PlayLists/Users command and query handlers.
2026-09-20 22:23:57 +02:00
Admin 11789e78fe web-player: base design system tokens
Add, additively (no component migration yet), the tokens from
section B.1 of the refactor plan: radii (tag/thumb), cover and icon
sizes, container widths, typographic tracking/leading, button colors
(--color-btn*), typography @utility recipes (text-display-*,
text-eyebrow, text-body, text-count), icon stroke
(stroke-thin/regular/bold), surfaces/grids (glass-bar, grid-cards*,
theme-transition), and the .animate-enter stagger via --i. Add
layout variables (--mf-player-side-w, --mf-menu-w, --mf-submenu-w,
--mf-range-*) and semantic z-index in theme.css. Clean up unused
color utilities (--color-accent-dim/glow/hair/line/muted,
--color-on-accent) and remove --mf-sidebar-bg (no importers) from
theme.css and tokens.ts. Scrollbar moves to tokens with a light-mode
variant.

Verified with npm run build (Tailwind compiles the @utility recipes
with responsive variants inside @apply).
2026-09-20 22:21:04 +02:00
Admin 96c75a32c8 web-player: baseline fixes and stray bugs
Fix eslint (dead imports in liked), missing title size in MediaCard,
non-exclusive classes in the Home spotlight rows, typography in the
album delete modal, useless text-base on icon buttons (Sidebar,
Queue), and remove Badge.svelte (unused).
2026-09-20 22:14:48 +02:00
246 changed files with 4012 additions and 9768 deletions
+47 -49
View File
@@ -1,57 +1,55 @@
# Musify
Music streaming platform: upload your own tracks, process them asynchronously
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
bytes ever going through the API.
A music streaming app. Upload your own tracks, organize them into playlists
and albums, and stream them back with a web player built around your
listening habits: auto-generated mixes, a home feed, search, likes, and an
Explore page for finding something new.
## What it does
- **Upload and organize your own music**: tracks, albums, cover art, artist credits.
- **Playlists**, likes, and a "recently played" history.
- **Mixes**, playlists generated automatically from what you actually listen to.
- **Explore**, genre browsing and search across tracks, albums, playlists and people.
- **Real streaming**, not just file downloads: seek, queue, shuffle, repeat.
- **Accounts via OIDC login**, with an optional anonymous listening mode.
- Light and dark themes, and a layout that works on both desktop and mobile.
## How it's built
**Backend**: .NET 10, split into a REST API, a background worker, and a
dedicated streaming gateway.
- Clean architecture (Domain / Application / Infrastructure) with CQRS (Mediator) and `ErrorOr` results.
- PostgreSQL via EF Core for metadata. RabbitMQ and MassTransit handle async processing (audio transcoding, thumbnails) with saga-based rollback.
- SeaweedFS (S3-compatible) for file storage. Uploads and downloads go straight between the client and storage, never through the API.
- ffmpeg for audio transcoding, ImageSharp for thumbnails.
- A YARP reverse proxy that streams the audio and checks a signed, short-lived ticket on every request.
- Zitadel for login (OIDC/OAuth2).
**Web client**: SvelteKit 2 on Svelte 5 (runes), TypeScript, Tailwind 4.
The session lives on the server, so the backend access token never reaches the browser.
**Infrastructure**: Musify's own apps run from Docker Compose (dev and
production overlays). Postgres, RabbitMQ, SeaweedFS, Zitadel and Jaeger are
shared infrastructure that lives in a separate `Infrastructure` git
repository, used by Musify and by other projects that need the same OIDC
provider and object storage.
## Project layout
```
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
docs/ What each piece does and why (in Spanish)
backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
web-player/ the SvelteKit web client
deploy/ Docker Compose stacks for dev and production
docs/ architecture notes, one file per area
```
```sh
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
```
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
layout. They live in the separate `Infrastructure` repository instead.
## Run it
## Digging deeper
Development (Docker Desktop):
```sh
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
```
Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC
provisioning — all as one-shot jobs baked into the compose file. Apps run from
the IDE by default; add `--profile apps up -d --build` as a second command to
run them in Docker instead.
Production, on a server with a domain:
```sh
cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
```
Idempotent — run the same commands again to deploy an update. See
[deploy/README.md](deploy/README.md) for the details, the port map and the
production prerequisites.
## Components
| Service | Port (dev) | Role |
|---|---|---|
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails |
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
[authentication](docs/authentication.md), [storage](docs/storage.md),
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
[development](docs/development.md).
[docs/](docs/README.md) covers how each piece works and why: architecture,
authentication, storage, streaming, media processing, and the local dev
setup. [deploy/README.md](deploy/README.md) has the full instructions for
running the stack, in development or on a production server.
@@ -7,7 +7,7 @@ using Musify.Application.Contracts;
namespace Musify.Application.Albums
{
public record GetRecentlyListenedAlbumsQuery(long UserId, int Limit)
: IQuery<ErrorOr<IReadOnlyList<AlbumApplicationResponse> >>;
: IQuery<ErrorOr<IReadOnlyList<AlbumApplicationResponse>>>;
public class GetRecentlyListenedAlbumsQueryHandler(IDatabase database)
: IQueryHandler<GetRecentlyListenedAlbumsQuery, ErrorOr<IReadOnlyList<AlbumApplicationResponse>>>
@@ -24,9 +24,9 @@ namespace Musify.Application.Albums
listenedTrackIds,
albumTrack => albumTrack.TrackId,
history => history.TrackId,
(albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt } )
(albumTrack, history) => new { albumTrack.AlbumId, history.ListenedAt })
.GroupBy(entry => entry.AlbumId)
.Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) } )
.Select(group => new { AlbumId = group.Key, LastListenedAt = group.Max(entry => entry.ListenedAt) })
.OrderByDescending(entry => entry.LastListenedAt)
.Take(request.Limit)
.ToListAsync(cancellationToken);
@@ -48,7 +48,7 @@ namespace Musify.Application.Albums
.Take(AlbumApplicationResponse.CoverTrackCount)
.Select(albumTrack => albumTrack.TrackId)
.ToList()
} )
})
.ToDictionaryAsync(entry => entry.Album.Id, cancellationToken);
return ranked
@@ -42,7 +42,7 @@ namespace Musify.Application.Albums
var effectiveSizeBytes = request.ExpectedSizeBytes
?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
var objectName = $"{Guid.NewGuid() }.{request.FileType.TrimStart('.').ToLowerInvariant() }";
var objectName = $"{Guid.NewGuid()}.{request.FileType.TrimStart('.').ToLowerInvariant()}";
var tempKey = albumConfiguration.Routes.BuildTempPicturePath(
uploadIntentConfiguration.TemporalRootPrefix, request.UserId, objectName);
@@ -84,7 +84,7 @@ namespace Musify.Application.Albums
await database.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (AlbumPicture, temp key: {Key} )", intent.Id, request.UserId, tempKey);
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (AlbumPicture, temp key: {Key})", intent.Id, request.UserId, tempKey);
return new AlbumPictureUploadResponse(
intent.Id,
@@ -1,6 +1,6 @@
namespace Musify.Application.Albums.Responses
{
/// <summary>A page of the user's catalog albums (their own).</summary>
public record AlbumsSearchResponse(
IReadOnlyList<AlbumSearchItemResponse> Items,
int PageNumber,
@@ -77,8 +77,6 @@ namespace Musify.Application.Albums
finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
}
database.Albums.Update(album);
if (pictureIntent != null && finalPictureKey != null)
{
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
@@ -3,8 +3,7 @@ using Musify.Application.Configuration.Responses;
namespace Musify.Application.Configuration
{
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
@@ -2,28 +2,16 @@ using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration
{
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
public sealed class PlaybackConfiguration
{
public const string SectionName = "Playback";
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
public bool AllowAnonymousListening { get; set; }
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
[Range(0, 3600)]
public int AnonymousFragmentSeconds { get; set; }
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
/// ever changes, since the two aren't wired together.</summary>
[Range(1, int.MaxValue)]
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
}
@@ -1,7 +1,5 @@
namespace Musify.Application.Configuration.Responses
{
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
}
@@ -2,11 +2,7 @@ namespace Musify.Application.Contracts
{
public interface IStreamTicketService
{
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
/// <c>sub</c> claim.</param>
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
}
@@ -12,11 +12,6 @@ namespace Musify.Application.Mixes
public record GenerateMixesForUserCommand(long UserId)
: ICommand<ErrorOr<Success>>;
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
public class GenerateMixesForUserCommandHandler(
IDatabase database,
MixConfiguration mixConfiguration,
@@ -5,9 +5,7 @@ using Musify.Domain.Entities;
namespace Musify.Application.Mixes
{
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
/// track lookup, instead of an EF Include chain deep enough (Mix -&gt; Items -&gt; Track -&gt; Owner) to
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
internal static class MixItemMapper
{
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
@@ -55,7 +55,7 @@ namespace Musify.Application.PlayLists
UserId = request.UserId,
Name = request.Name,
NormalizedName = request.Name.Trim().ToUpperInvariant(),
Description = string.IsNullOrWhiteSpace(request.Description)? null: request.Description.Trim(),
Description = string.IsNullOrWhiteSpace(request.Description) ? null : request.Description.Trim(),
Pictures = resolvedPictures.Pictures,
Visibility = request.Visibility
};
@@ -106,7 +106,7 @@ namespace Musify.Application.PlayLists
}, validation.Value);
}
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
private async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
Guid playListId,
UploadIntent pictureIntent,
string finalPictureKey,
@@ -16,9 +16,9 @@ namespace Musify.Application.PlayLists
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
public class GetPlayListTracksQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>> >
: IQueryHandler<GetPlayListTracksQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
{
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse> >> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetPlayListTracksQuery request, CancellationToken cancellationToken)
{
var playListExists = await database.PlayLists
.AsNoTracking()
@@ -10,12 +10,12 @@ using Musify.Application.Contracts;
namespace Musify.Application.PlayLists
{
public record GetPlayListsQuery(int PageNumber, int PageSize)
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>> >;
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>;
public class GetPlayListsQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListsQuery, ErrorOr<PaginatedResponse<PlayListApplicationResponse> >>
: IQueryHandler<GetPlayListsQuery, ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>
{
public async ValueTask<ErrorOr<PaginatedResponse<PlayListApplicationResponse> >> Handle(GetPlayListsQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>> Handle(GetPlayListsQuery request, CancellationToken cancellationToken)
{
var totalCount = await database.PlayLists.CountAsync(cancellationToken);
@@ -16,12 +16,12 @@ namespace Musify.Application.PlayLists
int PageNumber,
int PageSize,
bool OnlyPublic = false)
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>> >;
: IQuery<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>;
public class GetPlayListsByUserIdQueryHandler(IDatabase database)
: IQueryHandler<GetPlayListsByUserIdQuery, ErrorOr<PaginatedResponse<PlayListApplicationResponse> > >
: IQueryHandler<GetPlayListsByUserIdQuery, ErrorOr<PaginatedResponse<PlayListApplicationResponse>>>
{
public async ValueTask<ErrorOr<PaginatedResponse<PlayListApplicationResponse> >> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<PaginatedResponse<PlayListApplicationResponse>>> Handle(GetPlayListsByUserIdQuery request, CancellationToken cancellationToken)
{
var playListsQuery = database.PlayLists
.AsNoTracking()
@@ -51,7 +51,7 @@ namespace Musify.Application.PlayLists
.Take(PlayListApplicationResponse.CoverTrackCount)
.Select(plt => plt.TrackId)
.ToList()
} )
})
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
var pagedPlayLists = new StaticPagedList<PlayListApplicationResponse>(
@@ -42,7 +42,7 @@ namespace Musify.Application.PlayLists
var effectiveSizeBytes = request.ExpectedSizeBytes
?? uploadIntentConfiguration.DefaultExpectedPictureSizeBytes;
var objectName = $"{Guid.NewGuid() }.{request.FileType.TrimStart('.').ToLowerInvariant() }";
var objectName = $"{Guid.NewGuid()}.{request.FileType.TrimStart('.').ToLowerInvariant()}";
var tempKey = playListConfiguration.Routes.BuildTempPicturePath(
uploadIntentConfiguration.TemporalRootPrefix, request.UserId, objectName);
@@ -84,7 +84,7 @@ namespace Musify.Application.PlayLists
await database.SaveChangesAsync(cancellationToken);
await transaction.CommitAsync(cancellationToken);
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (PlayListPicture, temp key: {Key} )", intent.Id, request.UserId, tempKey);
logger.LogInformation("Issued upload intent {IntentId} for user {UserId} (PlayListPicture, temp key: {Key})", intent.Id, request.UserId, tempKey);
return new PlayListPictureUploadResponse(
intent.Id,
@@ -85,9 +85,7 @@ namespace Musify.Application.PlayLists
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
}
database.PlayLists.Update(playListEntity);
if (pictureIntent != null&& finalPictureKey != null)
if (pictureIntent != null && finalPictureKey != null)
{
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
playListEntity.Id, pictureIntent, finalPictureKey, cancellationToken);
@@ -117,7 +115,7 @@ namespace Musify.Application.PlayLists
return PlayListApplicationResponse.FromEntity(playListEntity, coverTrackIds);
}
async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
private async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
Guid playListId,
UploadIntent pictureIntent,
string finalPictureKey,
@@ -4,13 +4,6 @@ using System.Text.Json.Serialization;
namespace Musify.Application.Serialization;
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
// them to the nearest representable double, corrupting the id (it becomes a
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
// on the specific id-shaped `long`/`long?` response properties (not globally —
// unrelated `long` fields like byte sizes must stay numbers), this serializes
// them as a JSON string instead, which round-trips exactly.
public sealed class LongAsStringConverter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
@@ -30,8 +30,6 @@ public sealed class TrackStreamIssuer(
folderPath,
streamGatewayConfiguration.AudioFileName);
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
if (userId != null)
{
var newListeningHistory = new ListeningHistory
@@ -14,6 +14,7 @@ namespace Musify.Application.Tracks
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
{
var track = await database.ListeningHistories
.AsNoTracking()
.Include(t => t.Track.Owner)
.Include(t => t.Track.Tags)
.Where(t => t.UserId == query.UserId)
@@ -16,6 +16,7 @@ namespace Musify.Application.Tracks
public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
{
var recentTrackIds = await database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == query.UserId)
.GroupBy(l => l.TrackId)
.OrderByDescending(g => g.Max(l => l.ListenedAt))
@@ -24,14 +25,15 @@ namespace Musify.Application.Tracks
.ToListAsync(cancellationToken);
var tracksById = await database.Tracks
.AsNoTracking()
.Include(t => t.Owner)
.Include(t => t.Tags)
.Where(t => recentTrackIds.Contains(t.Id))
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count } )
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count })
.ToDictionaryAsync(t => t.Id, cancellationToken);
return recentTrackIds
.Select(id => tracksById[id] )
.Select(id => tracksById[id])
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.ListensCount));
}
}
@@ -10,7 +10,7 @@ namespace Musify.Application.Tracks
: IQuery<ErrorOr<TrackApplicationResponse>>;
public class GetTrackByIdQueryHandler(IDatabase database)
: IQueryHandler<GetTrackByIdQuery, ErrorOr<TrackApplicationResponse> >
: IQueryHandler<GetTrackByIdQuery, ErrorOr<TrackApplicationResponse>>
{
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetTrackByIdQuery request, CancellationToken cancellationToken)
{
@@ -48,6 +48,6 @@ namespace Musify.Application.Tracks
}
private static string? BuildKey(Func<string, string> builder, string? name)
=> string.IsNullOrEmpty(name)? null: builder(name);
=> string.IsNullOrEmpty(name) ? null : builder(name);
}
}
@@ -9,8 +9,7 @@ using Musify.Application.Tracks.Responses;
namespace Musify.Application.Tracks
{
/// <param name="UserId">Null for an anonymous request — allowed only when
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
: IQuery<ErrorOr<TrackStreamResponse>>;
@@ -33,7 +33,7 @@ namespace Musify.Application.Tracks
var pagedEntities = await tracksQuery
.OrderByDescending(t => t.Track.CreatedAt)
.ThenBy(t => t.Id)
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count } )
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
var items = pagedEntities
@@ -14,12 +14,12 @@ namespace Musify.Application.Tracks
string? Name,
int PageNumber,
int PageSize)
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse> > >;
: IQuery<ErrorOr<PaginatedResponse<TrackApplicationResponse>>>;
public class GetTracksByUserIdQueryHandler(IDatabase database)
: IQueryHandler<GetTracksByUserIdQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>> >
: IQueryHandler<GetTracksByUserIdQuery, ErrorOr<PaginatedResponse<TrackApplicationResponse>>>
{
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse> >> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<PaginatedResponse<TrackApplicationResponse>>> Handle(GetTracksByUserIdQuery request, CancellationToken cancellationToken)
{
var pageNumber = request.PageNumber < 1 ? 1 : request.PageNumber;
var pageSize = request.PageSize < 1 ? 10 : request.PageSize;
@@ -1,6 +1,6 @@
namespace Musify.Application.Tracks.Responses
{
/// <summary>A page of the user's catalog tracks (their own uploads).</summary>
public record TracksSearchResponse(
IReadOnlyList<TrackSearchItemResponse> Items,
int PageNumber,
@@ -28,7 +28,7 @@ namespace Musify.Application.Users
}
public class CreateUserCommandHandler(IDatabase database, ILogger<CreateUserCommandHandler> logger)
: ICommandHandler<CreateUserCommand, ErrorOr<UserApplicationResponse> >
: ICommandHandler<CreateUserCommand, ErrorOr<UserApplicationResponse>>
{
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(CreateUserCommand request, CancellationToken cancellationToken)
{
@@ -16,6 +16,7 @@ namespace Musify.Application.Users
var weekAgo = DateTime.UtcNow.AddDays(-7);
var thisWeekHistory = database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
var tracksThisWeek = await thisWeekHistory
@@ -24,10 +25,11 @@ namespace Musify.Application.Users
.CountAsync(cancellationToken);
var secondsThisWeek = await thisWeekHistory
.Join(database.Tracks, l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
.SumAsync(cancellationToken);
var listenedDates = await database.ListeningHistories
.AsNoTracking()
.Where(l => l.UserId == request.UserId)
.Select(l => l.ListenedAt.Date)
.Distinct()
@@ -7,10 +7,10 @@ using Musify.Application.Users.Responses;
namespace Musify.Application.Users
{
public record GetUserByIdQuery(long Id)
: IQuery<ErrorOr<UserApplicationResponse> >;
: IQuery<ErrorOr<UserApplicationResponse>>;
public class GetUserByIdQueryHandler(IDatabase database)
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse> >
: IQueryHandler<GetUserByIdQuery, ErrorOr<UserApplicationResponse>>
{
public async ValueTask<ErrorOr<UserApplicationResponse>> Handle(GetUserByIdQuery request, CancellationToken cancellationToken)
{
@@ -9,29 +9,29 @@ using Musify.Application.Contracts;
namespace Musify.Application.Users
{
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch)
: IQuery<ErrorOr<PaginatedResponse<UserApplicationResponse> >>;
: IQuery<ErrorOr<PaginatedResponse<UserApplicationResponse>>>;
public class GetUsersQueryHandler(IDatabase database)
: IQueryHandler<GetUsersQuery, ErrorOr<PaginatedResponse<UserApplicationResponse> > >
: IQueryHandler<GetUsersQuery, ErrorOr<PaginatedResponse<UserApplicationResponse>>>
{
public async ValueTask<ErrorOr<PaginatedResponse<UserApplicationResponse> >> Handle(GetUsersQuery request, CancellationToken cancellationToken)
public async ValueTask<ErrorOr<PaginatedResponse<UserApplicationResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
{
var user = database.Users
var usersQuery = database.Users
.AsNoTracking()
.AsQueryable();
if (!string.IsNullOrEmpty(request.UsernameSearch))
{
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
user = user.Where(u => u.NormalizedName.Contains(normalizedUsername));
usersQuery = usersQuery.Where(u => u.NormalizedName.Contains(normalizedUsername));
}
var totalCount = await user.CountAsync(cancellationToken);
var pagedUser = await user
var totalCount = await usersQuery.CountAsync(cancellationToken);
var pagedUsers = await usersQuery
.Select(u => UserApplicationResponse.FromEntity(u))
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUser);
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUsers);
}
}
}
@@ -1,9 +1,6 @@
namespace Musify.Domain.ValueObjects
{
/// <summary>
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
/// be tagged as both Classical and Metal at the same time).
/// </summary>
public static class GenreCompatibility
{
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
@@ -31,9 +28,6 @@ namespace Musify.Domain.ValueObjects
public static bool AreCompatible(Genre a, Genre b) =>
a == b || !IncompatiblePairs.Contains((a, b));
/// <summary>
/// Returns every mutually exclusive genre pair found within the given set of tags.
/// </summary>
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
{
var distinctTags = tags.Distinct().ToList();
@@ -24,11 +24,6 @@ namespace Musify.Infrastructure.Persistence
if (context == null)
return;
// SavingChanges(Async) fires before EF's own automatic change detection for this
// save, so entities modified through plain property assignment (query, then set a
// property, then SaveChanges — the normal case) still show as Unchanged here unless
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
// never bumped on a real update, only on inserts.
context.ChangeTracker.DetectChanges();
var now = DateTime.UtcNow;
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260531234306_AddUserProfilePictureUrl")]
partial class AddUserProfilePictureUrl
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserProfilePictureUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260601195757_RenameTimestampsAndAudit")]
partial class RenameTimestampsAndAudit
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RenameTimestampsAndAudit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
newName: "CreatedAt");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705123932_AddTrackProcessingStateSaga")]
partial class AddTrackProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
partial class AddTrackProcessingSagaKeys
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackProcessingSagaKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -29,7 +29,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
partial class AddPlayListProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPlayListProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260713234847_AddListeningHistory")]
partial class AddListeningHistory
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddListeningHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -47,7 +47,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260714114537_AddTrackDuration")]
partial class AddTrackDuration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackDuration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
partial class AddTrackSourceArtistExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackSourceArtistExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
filter: "\"ExternalId\" <> ''");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260715230836_AddTrackDownloadRequested")]
partial class AddTrackDownloadRequested
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackDownloadRequested : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
partial class MakeTrackMediaAndExternalIdNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakeTrackMediaAndExternalIdNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -66,7 +66,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
partial class AddArtistsAndTrackOwnership
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddArtistsAndTrackOwnership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
@@ -154,7 +154,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716220047_DropTrackArtistColumn")]
partial class DropTrackArtistColumn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DropTrackArtistColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
table: "Tracks");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
partial class MovePlayListPicturesToOwnedType
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,16 +4,15 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MovePlayListPicturesToOwnedType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
partial class RenameTrackDurationToDurationSeconds
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RenameTrackDurationToDurationSeconds : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -16,7 +16,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
newName: "DurationSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
partial class SplitTrackIntoLocalAndExternal
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class SplitTrackIntoLocalAndExternal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -156,7 +156,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717223358_MakePlayListPicturesNullable")]
partial class MakePlayListPicturesNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListPicturesNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -51,7 +51,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldMaxLength: 64);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720180629_WidenTrackTitle")]
partial class WidenTrackTitle
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class WidenTrackTitle : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldMaxLength: 50);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720180747_AddAlbums")]
partial class AddAlbums
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAlbums : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -100,7 +100,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "OwnerUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720184656_AddUserAlbumExternalId")]
partial class AddUserAlbumExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserAlbumExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721155102_AddMixes")]
partial class AddMixes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddMixes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -83,7 +83,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "TrackId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721161653_RemoveUserAlbumExternalId")]
partial class RemoveUserAlbumExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveUserAlbumExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -24,7 +24,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "OwnerUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721162102_DropMixGeneratedOn")]
partial class DropMixGeneratedOn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DropMixGeneratedOn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
table: "Mixes");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateOnly>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721190735_MakePlayListDescriptionOptional")]
partial class MakePlayListDescriptionOptional
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListDescriptionOptional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724170051_DurationAsDouble")]
partial class DurationAsDouble
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,16 +4,15 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DurationAsDouble : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724181012_AddExternalAlbums")]
partial class AddExternalAlbums
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddExternalAlbums : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -36,7 +36,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724182659_AddExternalAlbumThumbnail")]
partial class AddExternalAlbumThumbnail
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddExternalAlbumThumbnail : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
partial class AddAlbumProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAlbumProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914144913_AddPlayListVisibility")]
partial class AddPlayListVisibility
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPlayListVisibility : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914150045_AddTrackLikes")]
partial class AddTrackLikes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackLikes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914151900_AddUserFollows")]
partial class AddUserFollows
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserFollows : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260915165436_RemoveYouTubeIntegration")]
partial class RemoveYouTubeIntegration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,25 +5,16 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveYouTubeIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Data cleanup first, while the External*/Local*/UserAlbums tables (and the loose
// MixItem columns) still exist to query/cascade from. Deleting the base Tracks/Albums
// rows for anything that only ever existed as an ExternalTrack/ExternalAlbum cascades
// (FKs are ON DELETE CASCADE) to their own ExternalTracks/ExternalAlbums row, TrackArtist,
// PlayListHasTrack, AlbumHasTrack, TrackLike, ListeningHistory, UserHasTrack and MixItems
// rows that referenced them — this is deliberately lossy: those rows only existed to back
// the YouTube integration being removed here.
migrationBuilder.Sql("""DELETE FROM "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
// Live YouTube search hits that were seeded into a mix but never materialized into a
// real track (MixItemSource.YouTube, no TrackId) — MixItem.TrackId is about to become
// required, so these rows can't be kept.
migrationBuilder.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
migrationBuilder.AddColumn<long>(
@@ -132,7 +123,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260916153700_AddTrackTags")]
partial class AddTrackTags
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackTags : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,106 +0,0 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListPictureColumnsOptional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SmallPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "OriginalPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "MediumPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "LargePictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
name: "SmallPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "OriginalPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "MediumPictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
migrationBuilder.AlterColumn<string>(
name: "LargePictureName",
table: "PlayLists",
type: "character varying(64)",
maxLength: 64,
nullable: true,
oldClrType: typeof(string),
oldType: "character varying(64)",
oldMaxLength: 64,
oldNullable: true);
}
}
}
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("ProductVersion", "10.0.12")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" });
@@ -68,7 +68,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("Delivered");
b.ToTable("InboxState");
b.ToTable("InboxState", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
@@ -159,7 +159,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
.IsUnique();
b.ToTable("OutboxMessage");
b.ToTable("OutboxMessage", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
@@ -189,7 +189,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("Created");
b.ToTable("OutboxState");
b.ToTable("OutboxState", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.Album", b =>
@@ -231,7 +231,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("OwnerUserId");
b.ToTable("Albums");
b.ToTable("Albums", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b =>
@@ -259,7 +259,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("AlbumId", "TrackId")
.IsUnique();
b.ToTable("AlbumHasTrack");
b.ToTable("AlbumHasTrack", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b =>
@@ -283,7 +283,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.ToTable("ListeningHistory");
b.ToTable("ListeningHistory", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.Mix", b =>
@@ -317,7 +317,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId", "Position");
b.ToTable("Mixes");
b.ToTable("Mixes", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.MixItem", b =>
@@ -341,7 +341,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("MixId", "Position");
b.ToTable("MixItems");
b.ToTable("MixItems", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
@@ -385,7 +385,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.ToTable("PlayLists");
b.ToTable("PlayLists", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
@@ -412,7 +412,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("TrackId");
b.ToTable("PlayListHasTrack");
b.ToTable("PlayListHasTrack", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
@@ -450,7 +450,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("OwnerUserId");
b.ToTable("Tracks");
b.ToTable("Tracks", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b =>
@@ -475,7 +475,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId", "TrackId")
.IsUnique();
b.ToTable("TrackLikes");
b.ToTable("TrackLikes", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b =>
@@ -495,7 +495,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("TrackId", "Tag")
.IsUnique();
b.ToTable("TrackTags");
b.ToTable("TrackTags", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
@@ -524,7 +524,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.ToTable("Upload");
b.ToTable("Upload", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
@@ -575,7 +575,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.ToTable("UploadIntents");
b.ToTable("UploadIntents", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
@@ -613,7 +613,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("Id");
b.ToTable("User");
b.ToTable("User", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b =>
@@ -638,7 +638,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("FollowerId", "FollowedId")
.IsUnique();
b.ToTable("UserFollows");
b.ToTable("UserFollows", (string)null);
});
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
@@ -659,7 +659,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasIndex("UserId");
b.ToTable("UserHasTrack");
b.ToTable("UserHasTrack", (string)null);
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b =>
@@ -686,7 +686,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("CorrelationId");
b.ToTable("AlbumProcessingState");
b.ToTable("AlbumProcessingState", (string)null);
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b =>
@@ -713,7 +713,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("CorrelationId");
b.ToTable("PlayListProcessingState");
b.ToTable("PlayListProcessingState", (string)null);
});
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
@@ -749,7 +749,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b.HasKey("CorrelationId");
b.ToTable("TrackProcessingState");
b.ToTable("TrackProcessingState", (string)null);
});
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
@@ -803,7 +803,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b1.HasKey("AlbumId");
b1.ToTable("Albums");
b1.ToTable("Albums", (string)null);
b1.WithOwner()
.HasForeignKey("AlbumId");
@@ -896,28 +896,32 @@ namespace Musify.Infrastructure.Persistence.Migrations
.HasColumnType("uuid");
b1.Property<string>("LargeName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("LargePictureName");
b1.Property<string>("MediumName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("MediumPictureName");
b1.Property<string>("OriginalName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("OriginalPictureName");
b1.Property<string>("SmallName")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("character varying(64)")
.HasColumnName("SmallPictureName");
b1.HasKey("PlayListId");
b1.ToTable("PlayLists");
b1.ToTable("PlayLists", (string)null);
b1.WithOwner()
.HasForeignKey("PlayListId");
@@ -988,7 +992,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b1.HasKey("TrackId");
b1.ToTable("Tracks");
b1.ToTable("Tracks", (string)null);
b1.WithOwner()
.HasForeignKey("TrackId");
@@ -1025,7 +1029,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
b1.HasKey("TrackId");
b1.ToTable("Tracks");
b1.ToTable("Tracks", (string)null);
b1.WithOwner()
.HasForeignKey("TrackId");

Some files were not shown because too many files have changed in this diff Show More