Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d892d976a3 | ||
|
|
c07676f486 | ||
|
|
e4b0ce3df0 | ||
|
|
1a2bafe96c | ||
|
|
85f06edc53 | ||
|
|
a0ba57b569 | ||
|
|
8c6d4d6a6d | ||
|
|
137a3b62bb | ||
|
|
9fd0bf46d6 | ||
|
|
e187016c70 | ||
|
|
89326c0a16 | ||
|
|
65518e72e3 | ||
|
|
521cafc5c4 | ||
|
|
3e56725566 | ||
|
|
fbca8156a7 | ||
|
|
4b3d0b5fbd | ||
|
|
ec5b84f748 |
@@ -1,115 +0,0 @@
|
|||||||
name: CI/CD
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['**']
|
|
||||||
pull_request:
|
|
||||||
branches: ['**']
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: production-deploy
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
backend-tests:
|
|
||||||
name: BackEnd
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 20
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: backend
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-dotnet@v4
|
|
||||||
with:
|
|
||||||
dotnet-version: '10.0.x'
|
|
||||||
|
|
||||||
- name: Restore
|
|
||||||
run: dotnet restore Musify.slnx
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: dotnet build Musify.slnx --no-restore -c Release
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: dotnet test Musify.slnx --no-build -c Release
|
|
||||||
|
|
||||||
web-player-checks:
|
|
||||||
name: WebPlayer
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: web-player
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: web-player/package-lock.json
|
|
||||||
|
|
||||||
- run: npm ci
|
|
||||||
|
|
||||||
- name: Type-check
|
|
||||||
run: npm run check
|
|
||||||
|
|
||||||
- name: Lint
|
|
||||||
run: npm run lint
|
|
||||||
|
|
||||||
# Runs on the production host itself, through the existing self-hosted
|
|
||||||
# Gitea Actions runner (act_runner), which also carries a "production"
|
|
||||||
# label registered in native (non-docker) mode so its steps see the host's
|
|
||||||
# real Docker daemon directly (Docker socket + a bind-mounted deploy
|
|
||||||
# directory — see deploy/README.md for how that runner is set up).
|
|
||||||
#
|
|
||||||
# DEPLOY_PATH is the persistent clone already on that host (the one whose
|
|
||||||
# deploy/.env.prod is filled in and matches SharedServices/.env.prod) —
|
|
||||||
# deploy reuses it instead of a fresh checkout, since .env.prod is
|
|
||||||
# untracked and only lives there.
|
|
||||||
deploy:
|
|
||||||
name: Deploy
|
|
||||||
needs: [backend-tests, web-player-checks]
|
|
||||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
|
||||||
runs-on: [production]
|
|
||||||
timeout-minutes: 20
|
|
||||||
env:
|
|
||||||
DEPLOY_PATH: /home/ubuntu/deploys/musify
|
|
||||||
COMPOSE: docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml
|
|
||||||
steps:
|
|
||||||
- name: Pull latest master
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
git config --global --add safe.directory "$DEPLOY_PATH"
|
|
||||||
cd "$DEPLOY_PATH"
|
|
||||||
git fetch origin master
|
|
||||||
git reset --hard origin/master
|
|
||||||
|
|
||||||
- name: Migrate database, generate keys
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$DEPLOY_PATH"
|
|
||||||
$COMPOSE up -d wait-for-postgres keys-init migrate
|
|
||||||
|
|
||||||
# Both jobs verify themselves through the public auth./s3. URLs, served
|
|
||||||
# by the shared edge nginx (Infrastructure's edge/), and are idempotent.
|
|
||||||
- name: Create OIDC app and S3 bucket
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$DEPLOY_PATH"
|
|
||||||
$COMPOSE up -d seaweedfs-init zitadel-init
|
|
||||||
for job in musify-seaweedfs-init musify-zitadel-init; do
|
|
||||||
code=$(docker wait "$job")
|
|
||||||
if [ "$code" != "0" ]; then
|
|
||||||
docker logs "$job"
|
|
||||||
echo "$job exited with $code"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
- name: Build and start apps
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
cd "$DEPLOY_PATH"
|
|
||||||
$COMPOSE --profile apps up -d --build api worker gateway web-player
|
|
||||||
+1
-3
@@ -7,9 +7,6 @@ obj/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
.vs/
|
.vs/
|
||||||
artifacts/
|
artifacts/
|
||||||
TestResults/
|
|
||||||
*.trx
|
|
||||||
*.coverage
|
|
||||||
|
|
||||||
# Rider / VS
|
# Rider / VS
|
||||||
.idea/
|
.idea/
|
||||||
@@ -28,4 +25,5 @@ Thumbs.db
|
|||||||
# Deploy secrets and runtime state
|
# Deploy secrets and runtime state
|
||||||
deploy/keys/
|
deploy/keys/
|
||||||
deploy/secrets/
|
deploy/secrets/
|
||||||
|
deploy/nginx/certs/
|
||||||
deploy/zitadel/.output/
|
deploy/zitadel/.output/
|
||||||
|
|||||||
@@ -1,55 +1,57 @@
|
|||||||
# Musify
|
# Musify
|
||||||
|
|
||||||
A music streaming app. Upload your own tracks, organize them into playlists
|
Music streaming platform: upload your own tracks, process them asynchronously
|
||||||
and albums, and stream them back with a web player built around your
|
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
|
||||||
listening habits: auto-generated mixes, a home feed, search, likes, and an
|
bytes ever going through the API.
|
||||||
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 solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
|
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
|
||||||
web-player/ the SvelteKit web client
|
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
||||||
deploy/ Docker Compose stacks for dev and production
|
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
|
||||||
docs/ architecture notes, one file per area
|
docs/ What each piece does and why (in Spanish)
|
||||||
```
|
```
|
||||||
|
|
||||||
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
|
```sh
|
||||||
layout. They live in the separate `Infrastructure` repository instead.
|
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
|
||||||
|
```
|
||||||
|
|
||||||
## Digging deeper
|
## Run it
|
||||||
|
|
||||||
[docs/](docs/README.md) covers how each piece works and why: architecture,
|
Development (Docker Desktop):
|
||||||
authentication, storage, streaming, media processing, and the local dev
|
|
||||||
setup. [deploy/README.md](deploy/README.md) has the full instructions for
|
```sh
|
||||||
running the stack, in development or on a production server.
|
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).
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Musify.Application.Albums.Responses
|
namespace Musify.Application.Albums.Responses
|
||||||
{
|
{
|
||||||
|
/// <summary>A page of the user's catalog albums (their own).</summary>
|
||||||
public record AlbumsSearchResponse(
|
public record AlbumsSearchResponse(
|
||||||
IReadOnlyList<AlbumSearchItemResponse> Items,
|
IReadOnlyList<AlbumSearchItemResponse> Items,
|
||||||
int PageNumber,
|
int PageNumber,
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ using Musify.Application.Configuration.Responses;
|
|||||||
|
|
||||||
namespace Musify.Application.Configuration
|
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 record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
||||||
|
|
||||||
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
||||||
|
|||||||
@@ -2,16 +2,28 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Musify.Application.Configuration
|
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 sealed class PlaybackConfiguration
|
||||||
{
|
{
|
||||||
public const string SectionName = "Playback";
|
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; }
|
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)]
|
[Range(0, 3600)]
|
||||||
public int AnonymousFragmentSeconds { get; set; }
|
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)]
|
[Range(1, int.MaxValue)]
|
||||||
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-1
@@ -1,5 +1,7 @@
|
|||||||
namespace Musify.Application.Configuration.Responses
|
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);
|
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ namespace Musify.Application.Contracts
|
|||||||
{
|
{
|
||||||
public interface IStreamTicketService
|
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);
|
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ namespace Musify.Application.Mixes
|
|||||||
public record GenerateMixesForUserCommand(long UserId)
|
public record GenerateMixesForUserCommand(long UserId)
|
||||||
: ICommand<ErrorOr<Success>>;
|
: 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(
|
public class GenerateMixesForUserCommandHandler(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
MixConfiguration mixConfiguration,
|
MixConfiguration mixConfiguration,
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ using Musify.Domain.Entities;
|
|||||||
|
|
||||||
namespace Musify.Application.Mixes
|
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 -> Items -> Track -> Owner) to
|
||||||
|
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
|
||||||
internal static class MixItemMapper
|
internal static class MixItemMapper
|
||||||
{
|
{
|
||||||
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Musify.Application.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 sealed class LongAsStringConverter : JsonConverter<long>
|
||||||
{
|
{
|
||||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ public sealed class TrackStreamIssuer(
|
|||||||
folderPath,
|
folderPath,
|
||||||
streamGatewayConfiguration.AudioFileName);
|
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)
|
if (userId != null)
|
||||||
{
|
{
|
||||||
var newListeningHistory = new ListeningHistory
|
var newListeningHistory = new ListeningHistory
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ using Musify.Application.Tracks.Responses;
|
|||||||
|
|
||||||
namespace Musify.Application.Tracks
|
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)
|
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
||||||
: IQuery<ErrorOr<TrackStreamResponse>>;
|
: IQuery<ErrorOr<TrackStreamResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Musify.Application.Tracks.Responses
|
namespace Musify.Application.Tracks.Responses
|
||||||
{
|
{
|
||||||
|
/// <summary>A page of the user's catalog tracks (their own uploads).</summary>
|
||||||
public record TracksSearchResponse(
|
public record TracksSearchResponse(
|
||||||
IReadOnlyList<TrackSearchItemResponse> Items,
|
IReadOnlyList<TrackSearchItemResponse> Items,
|
||||||
int PageNumber,
|
int PageNumber,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
namespace Musify.Domain.ValueObjects
|
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
|
public static class GenreCompatibility
|
||||||
{
|
{
|
||||||
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
||||||
@@ -28,6 +31,9 @@ namespace Musify.Domain.ValueObjects
|
|||||||
public static bool AreCompatible(Genre a, Genre b) =>
|
public static bool AreCompatible(Genre a, Genre b) =>
|
||||||
a == b || !IncompatiblePairs.Contains((a, 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)
|
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
||||||
{
|
{
|
||||||
var distinctTags = tags.Distinct().ToList();
|
var distinctTags = tags.Distinct().ToList();
|
||||||
|
|||||||
@@ -24,6 +24,11 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
if (context == null)
|
if (context == null)
|
||||||
return;
|
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();
|
context.ChangeTracker.DetectChanges();
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -37,7 +42,7 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
}
|
}
|
||||||
else if (entry.State == EntityState.Modified)
|
else if (entry.State == EntityState.Modified)
|
||||||
{
|
{
|
||||||
entry.Property(nameof(IAuditable.UpdatedAt)).CurrentValue = now;
|
entry.Entity.UpdatedAt = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,9 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
public class Database(DatabaseConfiguration configuration)
|
public class Database(DatabaseConfiguration configuration)
|
||||||
: DbContext, AppIDatabase
|
: DbContext, AppIDatabase
|
||||||
{
|
{
|
||||||
private static readonly AuditableEntityInterceptor AuditInterceptor = new();
|
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder
|
optionsBuilder.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"));
|
||||||
.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"))
|
|
||||||
.AddInterceptors(AuditInterceptor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||||
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
||||||
|
|
||||||
serviceDescriptors.AddDbContext<Database>();
|
serviceDescriptors.AddSingleton<AuditableEntityInterceptor>();
|
||||||
|
|
||||||
|
serviceDescriptors.AddDbContext<Database>((serviceProvider, options) =>
|
||||||
|
options.AddInterceptors(serviceProvider.GetRequiredService<AuditableEntityInterceptor>()));
|
||||||
|
|
||||||
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
||||||
partial class AddUserProfilePictureUrl
|
partial class AddUserProfilePictureUrl
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddUserProfilePictureUrl : Migration
|
public partial class AddUserProfilePictureUrl : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
nullable: true);
|
nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
||||||
partial class RenameTimestampsAndAudit
|
partial class RenameTimestampsAndAudit
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class RenameTimestampsAndAudit : Migration
|
public partial class RenameTimestampsAndAudit : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
newName: "CreatedAt");
|
newName: "CreatedAt");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
||||||
partial class AddTrackProcessingStateSaga
|
partial class AddTrackProcessingStateSaga
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackProcessingStateSaga : Migration
|
public partial class AddTrackProcessingStateSaga : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
||||||
partial class AddTrackProcessingSagaKeys
|
partial class AddTrackProcessingSagaKeys
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackProcessingSagaKeys : Migration
|
public partial class AddTrackProcessingSagaKeys : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -29,6 +29,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
nullable: true);
|
nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
||||||
partial class AddPlayListProcessingStateSaga
|
partial class AddPlayListProcessingStateSaga
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddPlayListProcessingStateSaga : Migration
|
public partial class AddPlayListProcessingStateSaga : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260713234847_AddListeningHistory")]
|
[Migration("20260713234847_AddListeningHistory")]
|
||||||
partial class AddListeningHistory
|
partial class AddListeningHistory
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddListeningHistory : Migration
|
public partial class AddListeningHistory : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -47,6 +47,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "UserId");
|
column: "UserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260714114537_AddTrackDuration")]
|
[Migration("20260714114537_AddTrackDuration")]
|
||||||
partial class AddTrackDuration
|
partial class AddTrackDuration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackDuration : Migration
|
public partial class AddTrackDuration : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<int>(
|
migrationBuilder.AddColumn<int>(
|
||||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
defaultValue: 0);
|
defaultValue: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
||||||
partial class AddTrackSourceArtistExternalId
|
partial class AddTrackSourceArtistExternalId
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackSourceArtistExternalId : Migration
|
public partial class AddTrackSourceArtistExternalId : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
filter: "\"ExternalId\" <> ''");
|
filter: "\"ExternalId\" <> ''");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260715230836_AddTrackDownloadRequested")]
|
[Migration("20260715230836_AddTrackDownloadRequested")]
|
||||||
partial class AddTrackDownloadRequested
|
partial class AddTrackDownloadRequested
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackDownloadRequested : Migration
|
public partial class AddTrackDownloadRequested : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<bool>(
|
migrationBuilder.AddColumn<bool>(
|
||||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
defaultValue: false);
|
defaultValue: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
||||||
partial class MakeTrackMediaAndExternalIdNullable
|
partial class MakeTrackMediaAndExternalIdNullable
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -66,6 +66,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
||||||
partial class AddArtistsAndTrackOwnership
|
partial class AddArtistsAndTrackOwnership
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddArtistsAndTrackOwnership : Migration
|
public partial class AddArtistsAndTrackOwnership : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<long>(
|
migrationBuilder.AddColumn<long>(
|
||||||
@@ -154,6 +154,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716220047_DropTrackArtistColumn")]
|
[Migration("20260716220047_DropTrackArtistColumn")]
|
||||||
partial class DropTrackArtistColumn
|
partial class DropTrackArtistColumn
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class DropTrackArtistColumn : Migration
|
public partial class DropTrackArtistColumn : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
table: "Tracks");
|
table: "Tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
||||||
partial class MovePlayListPicturesToOwnedType
|
partial class MovePlayListPicturesToOwnedType
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class MovePlayListPicturesToOwnedType : Migration
|
public partial class MovePlayListPicturesToOwnedType : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
||||||
partial class RenameTrackDurationToDurationSeconds
|
partial class RenameTrackDurationToDurationSeconds
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class RenameTrackDurationToDurationSeconds : Migration
|
public partial class RenameTrackDurationToDurationSeconds : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
@@ -16,6 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
newName: "DurationSeconds");
|
newName: "DurationSeconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
||||||
partial class SplitTrackIntoLocalAndExternal
|
partial class SplitTrackIntoLocalAndExternal
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class SplitTrackIntoLocalAndExternal : Migration
|
public partial class SplitTrackIntoLocalAndExternal : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
@@ -156,6 +156,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldNullable: true);
|
oldNullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
||||||
partial class MakePlayListPicturesNullable
|
partial class MakePlayListPicturesNullable
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class MakePlayListPicturesNullable : Migration
|
public partial class MakePlayListPicturesNullable : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -51,6 +51,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldMaxLength: 64);
|
oldMaxLength: 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
backend/Core/Musify.Infrastructure/Persistence/Migrations/20260720180629_WidenTrackTitle.Designer.cs
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720180629_WidenTrackTitle")]
|
[Migration("20260720180629_WidenTrackTitle")]
|
||||||
partial class WidenTrackTitle
|
partial class WidenTrackTitle
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class WidenTrackTitle : Migration
|
public partial class WidenTrackTitle : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldMaxLength: 50);
|
oldMaxLength: 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720180747_AddAlbums")]
|
[Migration("20260720180747_AddAlbums")]
|
||||||
partial class AddAlbums
|
partial class AddAlbums
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddAlbums : Migration
|
public partial class AddAlbums : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -100,6 +100,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "OwnerUserId");
|
column: "OwnerUserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720184656_AddUserAlbumExternalId")]
|
[Migration("20260720184656_AddUserAlbumExternalId")]
|
||||||
partial class AddUserAlbumExternalId
|
partial class AddUserAlbumExternalId
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddUserAlbumExternalId : Migration
|
public partial class AddUserAlbumExternalId : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721155102_AddMixes")]
|
[Migration("20260721155102_AddMixes")]
|
||||||
partial class AddMixes
|
partial class AddMixes
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddMixes : Migration
|
public partial class AddMixes : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -83,6 +83,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "TrackId");
|
column: "TrackId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
||||||
partial class RemoveUserAlbumExternalId
|
partial class RemoveUserAlbumExternalId
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class RemoveUserAlbumExternalId : Migration
|
public partial class RemoveUserAlbumExternalId : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -24,6 +24,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "OwnerUserId");
|
column: "OwnerUserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721162102_DropMixGeneratedOn")]
|
[Migration("20260721162102_DropMixGeneratedOn")]
|
||||||
partial class DropMixGeneratedOn
|
partial class DropMixGeneratedOn
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class DropMixGeneratedOn : Migration
|
public partial class DropMixGeneratedOn : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
table: "Mixes");
|
table: "Mixes");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<DateOnly>(
|
migrationBuilder.AddColumn<DateOnly>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
||||||
partial class MakePlayListDescriptionOptional
|
partial class MakePlayListDescriptionOptional
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class MakePlayListDescriptionOptional : Migration
|
public partial class MakePlayListDescriptionOptional : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724170051_DurationAsDouble")]
|
[Migration("20260724170051_DurationAsDouble")]
|
||||||
partial class DurationAsDouble
|
partial class DurationAsDouble
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,15 +4,16 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class DurationAsDouble : Migration
|
public partial class DurationAsDouble : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724181012_AddExternalAlbums")]
|
[Migration("20260724181012_AddExternalAlbums")]
|
||||||
partial class AddExternalAlbums
|
partial class AddExternalAlbums
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddExternalAlbums : Migration
|
public partial class AddExternalAlbums : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -36,6 +36,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
||||||
partial class AddExternalAlbumThumbnail
|
partial class AddExternalAlbumThumbnail
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddExternalAlbumThumbnail : Migration
|
public partial class AddExternalAlbumThumbnail : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
nullable: true);
|
nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
|
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
|
||||||
partial class AddAlbumProcessingStateSaga
|
partial class AddAlbumProcessingStateSaga
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddAlbumProcessingStateSaga : Migration
|
public partial class AddAlbumProcessingStateSaga : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260914144913_AddPlayListVisibility")]
|
[Migration("20260914144913_AddPlayListVisibility")]
|
||||||
partial class AddPlayListVisibility
|
partial class AddPlayListVisibility
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddPlayListVisibility : Migration
|
public partial class AddPlayListVisibility : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<int>(
|
migrationBuilder.AddColumn<int>(
|
||||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
defaultValue: 0);
|
defaultValue: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260914150045_AddTrackLikes")]
|
[Migration("20260914150045_AddTrackLikes")]
|
||||||
partial class AddTrackLikes
|
partial class AddTrackLikes
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackLikes : Migration
|
public partial class AddTrackLikes : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260914151900_AddUserFollows")]
|
[Migration("20260914151900_AddUserFollows")]
|
||||||
partial class AddUserFollows
|
partial class AddUserFollows
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddUserFollows : Migration
|
public partial class AddUserFollows : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260915165436_RemoveYouTubeIntegration")]
|
[Migration("20260915165436_RemoveYouTubeIntegration")]
|
||||||
partial class RemoveYouTubeIntegration
|
partial class RemoveYouTubeIntegration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+13
-3
@@ -5,16 +5,25 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class RemoveYouTubeIntegration : Migration
|
public partial class RemoveYouTubeIntegration : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
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 "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
|
||||||
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
|
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.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
|
||||||
|
|
||||||
migrationBuilder.AddColumn<long>(
|
migrationBuilder.AddColumn<long>(
|
||||||
@@ -123,6 +132,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
onDelete: ReferentialAction.Restrict);
|
onDelete: ReferentialAction.Restrict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260916153700_AddTrackTags")]
|
[Migration("20260916153700_AddTrackTags")]
|
||||||
partial class AddTrackTags
|
partial class AddTrackTags
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+3
-2
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
public partial class AddTrackTags : Migration
|
public partial class AddTrackTags : Migration
|
||||||
{
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterDatabase()
|
migrationBuilder.AlterDatabase()
|
||||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
|||||||
@@ -41,8 +41,13 @@ public sealed class JwtBearerEventsHandler(
|
|||||||
if (cache.TryGetValue(cacheKey, out _))
|
if (cache.TryGetValue(cacheKey, out _))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Access tokens from the IdP are minimal (just sub/scope/aud) — profile attributes live on
|
||||||
|
// the userinfo endpoint, so ask for them there instead of relying on token claims alone.
|
||||||
var userInfo = await FetchUserInfoAsync(tokenValidatedContext, tokenValidatedContext.HttpContext.RequestAborted);
|
var userInfo = await FetchUserInfoAsync(tokenValidatedContext, tokenValidatedContext.HttpContext.RequestAborted);
|
||||||
|
|
||||||
|
// Zitadel also issues short-form JWT claim names ("name", "given_name", "family_name", "email"),
|
||||||
|
// but the newer JsonWebTokenHandler (default since .NET 8) no longer auto-maps those to the
|
||||||
|
// legacy ClaimTypes.* URIs the way JwtSecurityTokenHandler used to — so check both forms too.
|
||||||
var username = userInfo?.Name
|
var username = userInfo?.Name
|
||||||
?? userInfo?.PreferredUsername
|
?? userInfo?.PreferredUsername
|
||||||
?? principal.FindFirstValue("name")
|
?? principal.FindFirstValue("name")
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
// Properties annotated with [JsonConverter(typeof(LongAsStringConverter))] (id-shaped
|
||||||
|
// `long` fields — see that converter for why) serialize as a JSON string, but the
|
||||||
|
// schema generator still reflects the declared CLR `long`/`long?` type as a bare
|
||||||
|
// int64 number. Reconcile the declared schema so generated clients (openapi-typescript)
|
||||||
|
// type exactly those properties as `string`, matching what actually goes over the wire —
|
||||||
|
// scoped to the attribute, not the CLR type, so unrelated `long` fields (byte sizes, …)
|
||||||
|
// are left alone.
|
||||||
options.AddSchemaTransformer((schema, context, _) =>
|
options.AddSchemaTransformer((schema, context, _) =>
|
||||||
{
|
{
|
||||||
if (context.JsonPropertyInfo?.CustomConverter is LongAsStringConverter or NullableLongAsStringConverter)
|
if (context.JsonPropertyInfo?.CustomConverter is LongAsStringConverter or NullableLongAsStringConverter)
|
||||||
|
|||||||
@@ -48,4 +48,6 @@ app
|
|||||||
|
|
||||||
app.Run();
|
app.Run();
|
||||||
|
|
||||||
|
// Makes the top-level-statements Program class public so
|
||||||
|
// WebApplicationFactory<Program> (Musify.Api.Tests) can see it.
|
||||||
public partial class Program;
|
public partial class Program;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@WebApi_HostAddress = http://localhost:5111
|
||||||
|
|
||||||
|
GET {{WebApi_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
@@ -6,6 +6,10 @@ using Musify.StreamingGateway.Configuration;
|
|||||||
|
|
||||||
namespace Musify.StreamingGateway.Authentication;
|
namespace Musify.StreamingGateway.Authentication;
|
||||||
|
|
||||||
|
/// <param name="Prefix">The storage key prefix the ticket authorizes reading from. Empty means the
|
||||||
|
/// ticket was missing, malformed, expired, or otherwise invalid.</param>
|
||||||
|
/// <param name="MaxBytes">When set, the caller is limited to the first <c>MaxBytes</c> bytes of the
|
||||||
|
/// object — the anonymous preview fragment cap carried by the <c>maxBytes</c> claim.</param>
|
||||||
public sealed record TicketValidationResult(string Prefix, long? MaxBytes)
|
public sealed record TicketValidationResult(string Prefix, long? MaxBytes)
|
||||||
{
|
{
|
||||||
public static readonly TicketValidationResult Invalid = new(string.Empty, null);
|
public static readonly TicketValidationResult Invalid = new(string.Empty, null);
|
||||||
|
|||||||
@@ -72,6 +72,12 @@ public sealed class TicketValidationMiddleware(
|
|||||||
await next(context);
|
await next(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Rewrites the request's <c>Range</c> header so nothing past <paramref name="maxBytes"/>
|
||||||
|
/// is ever asked of the filer — this is what actually enforces the anonymous preview length,
|
||||||
|
/// downstream of the ticket carrying that cap. A request with no <c>Range</c> header (a plain,
|
||||||
|
/// whole-file GET) gets one synthesized, so the filer always responds with a bounded 206 instead of
|
||||||
|
/// streaming the full object. Returns false when the requested range starts at or past the cap, in
|
||||||
|
/// which case the caller should answer 416 directly instead of proxying anything.</summary>
|
||||||
private static bool TryClampRange(HttpContext context, long maxBytes)
|
private static bool TryClampRange(HttpContext context, long maxBytes)
|
||||||
{
|
{
|
||||||
var rangeHeader = context.Request.Headers.Range;
|
var rangeHeader = context.Request.Headers.Range;
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ using Musify.Infrastructure.Services;
|
|||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder();
|
var builder = Host.CreateApplicationBuilder();
|
||||||
|
|
||||||
|
// Registering Mediator pulls in every handler in Musify.Application, including ones
|
||||||
|
// (e.g. CreatePlayList, CreateTrack) that depend on IEventBus, which the Worker never
|
||||||
|
// registers because it only consumes events, it never publishes them. Only the handlers
|
||||||
|
// this process actually dispatches (GenerateMixesForUserCommand) are ever constructed,
|
||||||
|
// so the default eager validate-on-build check is too strict for this host.
|
||||||
builder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions
|
builder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions
|
||||||
{
|
{
|
||||||
ValidateOnBuild = false,
|
ValidateOnBuild = false,
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ RUN dotnet restore Hosts/Musify.Api/Api.csproj
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet publish Hosts/Musify.Api/Api.csproj -c Release -o /app --no-restore
|
RUN dotnet publish Hosts/Musify.Api/Api.csproj -c Release -o /app --no-restore
|
||||||
|
# Config files are AppSettings*.json (PascalCase); on case-sensitive Linux the host
|
||||||
|
# looks for appsettings*.json. Add lowercase copies so they get loaded.
|
||||||
|
RUN cp /app/AppSettings.json /app/appsettings.json \
|
||||||
|
&& cp /app/AppSettings.Development.json /app/appsettings.Development.json
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ RUN dotnet restore Hosts/Musify.Worker/Worker.csproj
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN dotnet publish Hosts/Musify.Worker/Worker.csproj -c Release -o /app --no-restore
|
RUN dotnet publish Hosts/Musify.Worker/Worker.csproj -c Release -o /app --no-restore
|
||||||
|
# AppSettings*.json (PascalCase) -> lowercase copies for case-sensitive Linux.
|
||||||
|
RUN cp /app/AppSettings.json /app/appsettings.json \
|
||||||
|
&& cp /app/AppSettings.Development.json /app/appsettings.Development.json
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|||||||
-6
@@ -1,4 +1,3 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Musify.Domain.Entities;
|
using Musify.Domain.Entities;
|
||||||
using Musify.Infrastructure.Tests.TestSupport;
|
using Musify.Infrastructure.Tests.TestSupport;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
@@ -44,11 +43,6 @@ namespace Musify.Infrastructure.Tests.Persistence
|
|||||||
Assert.Equal(originalCreatedAt, user.CreatedAt);
|
Assert.Equal(originalCreatedAt, user.CreatedAt);
|
||||||
Assert.True(user.UpdatedAt > originalCreatedAt,
|
Assert.True(user.UpdatedAt > originalCreatedAt,
|
||||||
$"original-created-at={originalCreatedAt:O}, final-updated-at={user.UpdatedAt:O}");
|
$"original-created-at={originalCreatedAt:O}, final-updated-at={user.UpdatedAt:O}");
|
||||||
|
|
||||||
await using var reader = fixture.CreateDatabase();
|
|
||||||
var stored = await reader.Users.SingleAsync(u => u.Id == user.Id, TestContext.Current.CancellationToken);
|
|
||||||
Assert.True(stored.UpdatedAt > stored.CreatedAt,
|
|
||||||
$"stored-created-at={stored.CreatedAt:O}, stored-updated-at={stored.UpdatedAt:O}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+24
-29
@@ -1,39 +1,39 @@
|
|||||||
# Development configuration for deploy/compose.yml + compose.dev.yml.
|
# Development configuration for deploy/compose.yml + compose.dev.yml.
|
||||||
# Copy to deploy/.env and adjust.
|
# Copy to deploy/.env and adjust. Not versioned.
|
||||||
#
|
#
|
||||||
# cp deploy/.env.example deploy/.env
|
# cp deploy/.env.example deploy/.env
|
||||||
#
|
|
||||||
# Requires ../../../Infrastructure running first (Postgres, Zitadel,
|
|
||||||
# SeaweedFS, RabbitMQ, Jaeger). See ../../../Infrastructure/README.md.
|
|
||||||
# Everything marked "must match Infrastructure's .env" below has to be the
|
|
||||||
# same value as the one in that file.
|
|
||||||
|
|
||||||
DOTNET_ENVIRONMENT=Development
|
DOTNET_ENVIRONMENT=Development
|
||||||
|
|
||||||
# --- Public URLs -------------------------------------------------------------
|
# --- Public URLs -------------------------------------------------------------
|
||||||
# How each service is reached from *outside* Docker. `host.docker.internal`
|
# How each service is reached from *outside* Docker. `host.docker.internal`
|
||||||
# resolves to the same address from the browser, from apps running on the
|
# resolves to the same address from the browser, from apps running on the host
|
||||||
# host and from inside the containers, which is what makes the OIDC issuer
|
# and from inside the containers, which is what makes the OIDC issuer and the
|
||||||
# and the presigned S3 URLs valid in both run modes.
|
# presigned S3 URLs valid in both run modes.
|
||||||
PUBLIC_WEB_URL=http://localhost:3000
|
PUBLIC_WEB_URL=http://localhost:3000
|
||||||
PUBLIC_API_URL=http://localhost:5111
|
PUBLIC_API_URL=http://localhost:5111
|
||||||
PUBLIC_STREAM_URL=http://localhost:8081
|
PUBLIC_STREAM_URL=http://localhost:8081
|
||||||
PUBLIC_AUTH_URL=http://host.docker.internal:8080
|
PUBLIC_AUTH_URL=http://host.docker.internal:8080
|
||||||
|
|
||||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
|
||||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.
|
|
||||||
POSTGRES_HOST=host.docker.internal
|
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_DB=musify_db
|
POSTGRES_DB=musify_db
|
||||||
|
POSTGRES_HOST_PORT=59000
|
||||||
|
|
||||||
# --- Zitadel (OIDC, shared instance) -----------------------------------------
|
# --- Zitadel (OIDC) ----------------------------------------------------------
|
||||||
# Path to the admin service-account PAT Infrastructure's Zitadel writes on
|
# The masterkey MUST be exactly 32 characters. The external domain/port form
|
||||||
# init. Adjust if your Infrastructure checkout lives somewhere else relative
|
# the token issuer, so they have to match PUBLIC_AUTH_URL. Changing them needs
|
||||||
# to this one.
|
# a re-init: docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down -v
|
||||||
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
|
ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters
|
||||||
# Allows the OIDC apps this project creates to use plain-http redirect URIs
|
ZITADEL_EXTERNAL_DOMAIN=host.docker.internal
|
||||||
# (dev only).
|
ZITADEL_EXTERNAL_PORT=8080
|
||||||
|
ZITADEL_EXTERNAL_SECURE=false
|
||||||
|
ZITADEL_DATABASE_PASSWORD=zitadel
|
||||||
|
# Console login: <username>@zitadel.<external domain>
|
||||||
|
ZITADEL_ADMIN_USERNAME=admin
|
||||||
|
ZITADEL_ADMIN_PASSWORD=Password1!
|
||||||
|
# Allows the OIDC apps to use plain-http redirect URIs (dev only).
|
||||||
ZITADEL_DEV_MODE=true
|
ZITADEL_DEV_MODE=true
|
||||||
|
|
||||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||||
@@ -42,25 +42,17 @@ AUTH_CLIENT_ID=
|
|||||||
WEB_CLIENT_ID=
|
WEB_CLIENT_ID=
|
||||||
WEB_CLIENT_SECRET=
|
WEB_CLIENT_SECRET=
|
||||||
|
|
||||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||||
# Must match Infrastructure's .env.
|
|
||||||
RABBITMQ_HOST=host.docker.internal
|
|
||||||
RABBITMQ_DEFAULT_USER=guest
|
RABBITMQ_DEFAULT_USER=guest
|
||||||
RABBITMQ_DEFAULT_PASS=guest
|
RABBITMQ_DEFAULT_PASS=guest
|
||||||
|
|
||||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||||
# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env.
|
|
||||||
SEAWEEDFS_HOST=host.docker.internal
|
|
||||||
S3_ACCESS_KEY=admin_access_key
|
S3_ACCESS_KEY=admin_access_key
|
||||||
S3_SECRET_KEY=admin_secret_key
|
S3_SECRET_KEY=admin_secret_key
|
||||||
S3_BUCKET=webapi-storage
|
S3_BUCKET=webapi-storage
|
||||||
S3_PUBLIC_URL=http://host.docker.internal:8333
|
S3_PUBLIC_URL=http://host.docker.internal:8333
|
||||||
S3_INTERNAL_URL=http://host.docker.internal:8333
|
|
||||||
S3_USE_HTTP=true
|
S3_USE_HTTP=true
|
||||||
|
|
||||||
# --- Jaeger (shared instance, tracing) ---------------------------------------
|
|
||||||
JAEGER_HOST=host.docker.internal
|
|
||||||
|
|
||||||
# --- Web player --------------------------------------------------------------
|
# --- Web player --------------------------------------------------------------
|
||||||
# Key material for the encrypted session cookie: any long random string.
|
# Key material for the encrypted session cookie: any long random string.
|
||||||
SESSION_SECRET=dev-session-secret-change-me-0123456789abcdef
|
SESSION_SECRET=dev-session-secret-change-me-0123456789abcdef
|
||||||
@@ -69,3 +61,6 @@ AUTH_ALLOW_INSECURE_HTTP=true
|
|||||||
|
|
||||||
# --- Misc --------------------------------------------------------------------
|
# --- Misc --------------------------------------------------------------------
|
||||||
AUTH_REQUIRE_HTTPS=false
|
AUTH_REQUIRE_HTTPS=false
|
||||||
|
PGADMIN_DEFAULT_EMAIL=admin@musify.dev
|
||||||
|
PGADMIN_DEFAULT_PASSWORD=admin
|
||||||
|
PGADMIN_HOST_PORT=5050
|
||||||
|
|||||||
+28
-32
@@ -1,41 +1,41 @@
|
|||||||
# Production configuration for deploy/compose.yml + compose.prod.yml.
|
# Production configuration for deploy/compose.yml + compose.prod.yml.
|
||||||
# Copy to deploy/.env.prod, then:
|
# Copy to deploy/.env.prod, then:
|
||||||
#
|
#
|
||||||
# 1. replace every `*.example.com` with your actual domains (each service
|
# 1. replace every `example.com` with your domain,
|
||||||
# below has its own, independent domain — they don't need to share a
|
|
||||||
# root, e.g. for DNS providers that don't allow multi-level
|
|
||||||
# subdomains),
|
|
||||||
# 2. replace every CHANGE_ME with a strong random value
|
# 2. replace every CHANGE_ME with a strong random value
|
||||||
# (`openssl rand -hex 24`),
|
# (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32 chars),
|
||||||
# 3. point each domain below at the server and add its vhost to the shared
|
# 3. point these DNS records at the server, all proxied/TLS-terminated the
|
||||||
# edge (Infrastructure's edge/conf.d).
|
# same way: example.com, www, api, auth, stream, s3.
|
||||||
#
|
#
|
||||||
# Requires ../../../Infrastructure running first, on the same Docker host,
|
# Not versioned. Keep it readable only by the deploy user (chmod 600).
|
||||||
# with its own `infra-net` network already created. See
|
|
||||||
# ../../../Infrastructure/README.md. Not versioned. Keep it readable only by
|
|
||||||
# the deploy user (chmod 600).
|
|
||||||
|
|
||||||
DOTNET_ENVIRONMENT=Production
|
DOTNET_ENVIRONMENT=Production
|
||||||
|
|
||||||
# --- Domains -------------------------------------------------------------
|
# --- Domain ------------------------------------------------------------------
|
||||||
# Public URLs. They must match the vhosts of the shared edge nginx
|
# ROOT_DOMAIN drives the nginx vhosts; the URLs below must agree with it.
|
||||||
# (Infrastructure's edge/conf.d), which is what serves them; the domains are
|
ROOT_DOMAIN=example.com
|
||||||
# independent, they don't need to share a root.
|
PUBLIC_WEB_URL=https://example.com
|
||||||
PUBLIC_WEB_URL=https://musify.example.com
|
PUBLIC_API_URL=https://api.example.com
|
||||||
PUBLIC_API_URL=https://musifyapi.example.com
|
PUBLIC_STREAM_URL=https://stream.example.com
|
||||||
PUBLIC_STREAM_URL=https://musifystream.example.com
|
|
||||||
PUBLIC_AUTH_URL=https://auth.example.com
|
PUBLIC_AUTH_URL=https://auth.example.com
|
||||||
S3_PUBLIC_URL=https://s3.example.com
|
S3_PUBLIC_URL=https://s3.example.com
|
||||||
|
|
||||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
|
||||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.prod.
|
|
||||||
POSTGRES_HOST=postgres
|
|
||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=CHANGE_ME
|
POSTGRES_PASSWORD=CHANGE_ME
|
||||||
POSTGRES_DB=musify_db
|
POSTGRES_DB=musify_db
|
||||||
|
|
||||||
# --- Zitadel (OIDC, shared instance) -----------------------------------------
|
# --- Zitadel (OIDC) ----------------------------------------------------------
|
||||||
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
|
# The masterkey MUST be exactly 32 characters and can never be rotated in place.
|
||||||
|
ZITADEL_MASTERKEY=CHANGE_ME_32_CHARACTERS_EXACTLY_
|
||||||
|
ZITADEL_EXTERNAL_DOMAIN=auth.example.com
|
||||||
|
ZITADEL_EXTERNAL_PORT=443
|
||||||
|
ZITADEL_EXTERNAL_SECURE=true
|
||||||
|
ZITADEL_DATABASE_PASSWORD=CHANGE_ME
|
||||||
|
# Console login: <username>@zitadel.<external domain>
|
||||||
|
ZITADEL_ADMIN_USERNAME=admin
|
||||||
|
ZITADEL_ADMIN_PASSWORD=CHANGE_ME
|
||||||
|
# Keep OIDC apps strict about https redirect URIs in production.
|
||||||
ZITADEL_DEV_MODE=false
|
ZITADEL_DEV_MODE=false
|
||||||
|
|
||||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||||
@@ -44,26 +44,22 @@ AUTH_CLIENT_ID=
|
|||||||
WEB_CLIENT_ID=
|
WEB_CLIENT_ID=
|
||||||
WEB_CLIENT_SECRET=
|
WEB_CLIENT_SECRET=
|
||||||
|
|
||||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||||
RABBITMQ_HOST=rabbitmq
|
|
||||||
RABBITMQ_DEFAULT_USER=musify
|
RABBITMQ_DEFAULT_USER=musify
|
||||||
RABBITMQ_DEFAULT_PASS=CHANGE_ME
|
RABBITMQ_DEFAULT_PASS=CHANGE_ME
|
||||||
|
|
||||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||||
SEAWEEDFS_HOST=seaweedfs
|
|
||||||
S3_ACCESS_KEY=CHANGE_ME
|
S3_ACCESS_KEY=CHANGE_ME
|
||||||
S3_SECRET_KEY=CHANGE_ME
|
S3_SECRET_KEY=CHANGE_ME
|
||||||
S3_BUCKET=webapi-storage
|
S3_BUCKET=webapi-storage
|
||||||
S3_INTERNAL_URL=http://seaweedfs:8333
|
|
||||||
S3_USE_HTTP=false
|
S3_USE_HTTP=false
|
||||||
|
|
||||||
# --- Jaeger (shared instance, tracing) ---------------------------------------
|
|
||||||
JAEGER_HOST=jaeger
|
|
||||||
|
|
||||||
# --- Web player --------------------------------------------------------------
|
# --- Web player --------------------------------------------------------------
|
||||||
# Key material for the encrypted session cookie. Rotating it logs everyone out.
|
# Key material for the encrypted session cookie. Rotating it logs everyone out.
|
||||||
SESSION_SECRET=CHANGE_ME
|
SESSION_SECRET=CHANGE_ME
|
||||||
AUTH_ALLOW_INSECURE_HTTP=false
|
AUTH_ALLOW_INSECURE_HTTP=false
|
||||||
|
|
||||||
# --- Misc --------------------------------------------------------------------
|
# --- Misc --------------------------------------------------------------------
|
||||||
AUTH_REQUIRE_HTTPS=true
|
AUTH_REQUIRE_HTTPS=true
|
||||||
|
# Largest request body nginx accepts (audio uploads go through the web player).
|
||||||
|
MAX_UPLOAD_SIZE=1024m
|
||||||
|
|||||||
+87
-121
@@ -1,65 +1,51 @@
|
|||||||
# Deploy
|
# Deploy
|
||||||
|
|
||||||
Everything needed to run Musify itself, in development and in production,
|
Everything needed to run Musify, in development and in production, from the
|
||||||
from the same set of compose files, no wrapper scripts, just `docker
|
same set of compose files — no wrapper scripts, just `docker compose`.
|
||||||
compose`. Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are **not**
|
|
||||||
started here: they're shared infrastructure that lives in the separate
|
|
||||||
`Infrastructure` repository and has to already be running (see its own
|
|
||||||
README). `../../Infrastructure` below is just an example path, assuming
|
|
||||||
that repo happens to be checked out as a sibling of this one. Adjust it to
|
|
||||||
wherever you actually cloned it.
|
|
||||||
|
|
||||||
| File | Role |
|
| File | Role |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `compose.yml` | Musify's own services, including the one-shot bootstrap jobs. No published ports, no domains, no environment-specific values. |
|
| `compose.yml` | Every service, including the one-shot bootstrap jobs. No published ports, no domains, no environment-specific values. |
|
||||||
| `compose.dev.yml` | Development overlay: publishes Musify's ports to the host. |
|
| `compose.dev.yml` | Development overlay: publishes the ports to the host, adds pgAdmin. |
|
||||||
| `compose.prod.yml` | Production overlay: joins Infrastructure's `infra-net` network. |
|
| `compose.prod.yml` | Production overlay: adds the nginx edge. |
|
||||||
| `.env` / `.env.prod` | The values compose interpolates. Not versioned; templates in `.env.example` / `.env.prod.example`. |
|
| `.env` / `.env.prod` | The values compose interpolates. Not versioned; templates in `.env.example` / `.env.prod.example`. |
|
||||||
|
|
||||||
The four applications (`api`, `worker`, `gateway`, `web-player`) sit behind
|
The four applications (`api`, `worker`, `gateway`, `web-player`) sit behind the
|
||||||
the `apps` compose profile, so a plain `up` starts only the one-shot
|
`apps` compose profile, so a plain `up` starts infrastructure only — the usual
|
||||||
bootstrap jobs, the usual development setup, with the apps running from the
|
development setup, with the apps running from the IDE.
|
||||||
IDE.
|
|
||||||
|
|
||||||
### The one-shot bootstrap jobs
|
### The one-shot bootstrap jobs
|
||||||
|
|
||||||
Four services in `compose.yml` run once and exit, so a plain `docker compose
|
Three services in `compose.yml` run once and exit, so a plain `docker compose
|
||||||
up -d` leaves the stack fully ready with no manual steps:
|
up -d` leaves the stack fully ready with no manual steps:
|
||||||
|
|
||||||
| Service | Does |
|
| Service | Does |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `wait-for-postgres` | Waits for Infrastructure's Postgres to accept connections. Replaces an in-file health-check `depends_on`, which only works within the same compose file. |
|
|
||||||
| `keys-init` | Generates the RS256 stream-ticket key pair in `./keys`, if it is not there already. |
|
| `keys-init` | Generates the RS256 stream-ticket key pair in `./keys`, if it is not there already. |
|
||||||
| `migrate` | Applies the EF Core migrations (built from `backend/Musify.Migrator.Dockerfile`). |
|
| `migrate` | Applies the EF Core migrations (built from `backend/Musify.Migrator.Dockerfile`). |
|
||||||
| `seaweedfs-init` | Creates Musify's own bucket on the shared SeaweedFS, if it doesn't exist yet. |
|
| `zitadel-init` | Creates the Musify project and its two OIDC apps in Zitadel (or reuses them), then writes `AUTH_CLIENT_ID` / `WEB_CLIENT_ID` / `WEB_CLIENT_SECRET` into the env file. |
|
||||||
| `zitadel-init` | Creates the Musify project and its OIDC apps in the shared Zitadel (or reuses them), then writes `AUTH_CLIENT_ID` / `WEB_CLIENT_ID` / `WEB_CLIENT_SECRET` / `NATIVE_CLIENT_ID` into the env file. |
|
|
||||||
|
|
||||||
`api` and `worker` wait for `migrate`; `api` waits for `keys-init`
|
`api` and `worker` wait for `migrate`; `api` and `gateway` wait for
|
||||||
(`depends_on: condition: service_completed_successfully`). Nothing waits on
|
`keys-init` (`depends_on: condition: service_completed_successfully`). They do
|
||||||
`zitadel-init`, `seaweedfs-init` or the shared infrastructure services
|
**not** wait for `zitadel-init` — compose interpolates `${AUTH_CLIENT_ID}` once,
|
||||||
directly. Compose interpolates `${AUTH_CLIENT_ID}` once, when a command
|
when a command starts, so a container that starts in the *same* `up` as
|
||||||
starts, so a container that started in the *same* `up` as `zitadel-init`
|
`zitadel-init` would still get the empty value it had before provisioning ran.
|
||||||
would still get the empty value it had before provisioning ran. That's why
|
That's why bringing up the apps is a second command: by the time you run it,
|
||||||
bringing up the apps is a second command: by the time you run it,
|
|
||||||
`zitadel-init` has already finished and the env file has real values.
|
`zitadel-init` has already finished and the env file has real values.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Start the separate `Infrastructure` repository first, from wherever you
|
|
||||||
cloned it (see its own README for the exact command). Then, from this
|
|
||||||
repository's root:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
||||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build
|
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml --profile apps up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
The first command starts Musify's own bootstrap jobs (Postgres wait, keys,
|
The first command starts the infrastructure and runs the bootstrap jobs
|
||||||
migrations, the bucket, the Zitadel project/apps). The second builds and runs
|
(keys, migrations, Zitadel project/apps). The second builds and runs `api`,
|
||||||
`api`, `worker`, `gateway` and `web-player` in Docker too. Skip it to run the
|
`worker`, `gateway` and `web-player` in Docker too — skip it to run the apps
|
||||||
apps from the IDE instead (the usual dev loop):
|
from the IDE instead (the usual dev loop):
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
dotnet run --project backend/Hosts/Musify.Api # :5111
|
dotnet run --project backend/Hosts/Musify.Api # :5111
|
||||||
@@ -69,117 +55,86 @@ npm --prefix web-player run dev # :5173
|
|||||||
```
|
```
|
||||||
|
|
||||||
When running the apps from the IDE, `Musify.Api` reads `Authentication:*`
|
When running the apps from the IDE, `Musify.Api` reads `Authentication:*`
|
||||||
from user-secrets and `web-player` reads its own `web-player/.env`, neither
|
from user-secrets and `web-player` reads its own `web-player/.env` — neither
|
||||||
looks at `deploy/.env`. Copy the four values `zitadel-init` wrote
|
looks at `deploy/.env`. Copy the three values `zitadel-init` wrote
|
||||||
(`AUTH_CLIENT_ID`, `WEB_CLIENT_ID`, `WEB_CLIENT_SECRET`, `NATIVE_CLIENT_ID`)
|
(`AUTH_CLIENT_ID`, `WEB_CLIENT_ID`, `WEB_CLIENT_SECRET`) into
|
||||||
into `dotnet user-secrets set Authentication:ClientId <value> --project
|
`dotnet user-secrets set Authentication:ClientId <value> --project
|
||||||
backend/Hosts/Musify.Api` and into `web-player/.env` once. They stay valid
|
backend/Hosts/Musify.Api` and into `web-player/.env` once; they stay valid
|
||||||
until Infrastructure's Zitadel volume is wiped.
|
until you wipe the Zitadel volume.
|
||||||
|
|
||||||
|
Add `--profile tools` to also start pgAdmin.
|
||||||
|
|
||||||
### Ports
|
### Ports
|
||||||
|
|
||||||
| Service | Port | Used by |
|
| Service | Port | Used by |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Musify.Api | `5111` | `/scalar/v1`, `/openapi/v1.json` |
|
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and Zitadel |
|
||||||
| Musify.StreamingGateway | `8081` | `/health`, `/media/...` |
|
| Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` |
|
||||||
| web-player | `5173` (`npm run dev`) or `3000` (container) | |
|
| RabbitMQ | `5672` / `15672` | MassTransit / management UI |
|
||||||
|
| SeaweedFS | `8333` / `8888` / `9333` | S3 / filer (→ gateway) / master |
|
||||||
|
| Jaeger | `16686` / `4317` / `4318` | traces UI / OTLP gRPC / OTLP HTTP |
|
||||||
|
| pgAdmin (`--profile tools`) | `5050` | Postgres UI |
|
||||||
|
| API, gateway, web player (`--profile apps`) | `5111` / `8081` / `3000` | |
|
||||||
|
|
||||||
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger ports come from
|
Host requirements: .NET 10 SDK, Node 22, Docker Desktop. (ffmpeg is only
|
||||||
Infrastructure. See its own README for that table.
|
needed on the host if you run the Worker outside Docker — inside Docker its
|
||||||
|
image already has it.)
|
||||||
Host requirements: .NET 10 SDK, Node 22, Docker Desktop. ffmpeg is only
|
|
||||||
needed on the host if you run the Worker outside Docker; inside Docker its
|
|
||||||
image already has it.
|
|
||||||
|
|
||||||
### Why `host.docker.internal`
|
### Why `host.docker.internal`
|
||||||
|
|
||||||
`PUBLIC_AUTH_URL` and `S3_PUBLIC_URL` point at `host.docker.internal`, which
|
`PUBLIC_AUTH_URL` and `S3_PUBLIC_URL` point at `host.docker.internal`, which
|
||||||
resolves to the same address from the browser, from apps running on the host
|
resolves to the same address from the browser, from apps running on the host
|
||||||
and from inside the containers. That keeps the OIDC token issuer and the
|
and from inside the containers. That keeps the OIDC token issuer and the
|
||||||
presigned S3 URLs valid whichever way the apps are running, and is the same
|
presigned S3 URLs valid whichever way the apps are running. Zitadel routes by
|
||||||
address Infrastructure's own dev ports are published on.
|
the `Host` header, so the domain has to be identical everywhere; changing it
|
||||||
|
requires wiping the Postgres/Zitadel volumes and starting over (`docker
|
||||||
|
compose -f compose.yml -f compose.dev.yml down -v`, then `up -d` again).
|
||||||
|
|
||||||
### Resetting
|
### Resetting
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down # stop, keep data
|
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down # stop, keep data
|
||||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down -v # stop, wipe Musify's own volumes
|
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down -v # stop, wipe volumes
|
||||||
```
|
```
|
||||||
|
|
||||||
This only touches Musify's own containers and volumes. Resetting Zitadel or
|
|
||||||
SeaweedFS is done from the `Infrastructure` repo instead.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Production
|
## Production
|
||||||
|
|
||||||
Musify doesn't run its own nginx: the shared edge in Infrastructure
|
On a server with a domain, from the repository root:
|
||||||
(`edge/`) owns host ports 80/443, terminates TLS for every project's domains
|
|
||||||
and proxies to Musify's containers over `infra-net`. On a server with
|
|
||||||
domains:
|
|
||||||
|
|
||||||
1. Bring up the `Infrastructure` repo first, with its own `.env.prod` filled
|
|
||||||
in. It creates the `infra-net` network this stack joins.
|
|
||||||
2. Bring up its `edge/` (see Infrastructure's README) and add Musify's
|
|
||||||
vhosts and certificate there (`edge/conf.d/musify.conf`). The five
|
|
||||||
public hostnames (web, api, stream, auth, s3) are independent: they
|
|
||||||
don't need to share a root domain.
|
|
||||||
3. From this repository's root:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cp deploy/.env.prod.example deploy/.env.prod
|
cp deploy/.env.prod.example deploy/.env.prod
|
||||||
```
|
```
|
||||||
|
|
||||||
Replace every `*.example.com` with your domains (they must match the
|
Fill it in:
|
||||||
edge's vhosts) and every `CHANGE_ME` with a random value
|
|
||||||
(`openssl rand -hex 24`). `chmod 600 deploy/.env.prod`.
|
|
||||||
|
|
||||||
Then:
|
1. Replace every `example.com` with your domain and every `CHANGE_ME` with a
|
||||||
|
random value (`openssl rand -hex 24`; the Zitadel masterkey needs exactly
|
||||||
|
32 characters). `chmod 600 deploy/.env.prod`.
|
||||||
|
2. Drop **`deploy/nginx/certs/origin.pem` and `origin.key`** — the origin
|
||||||
|
certificate nginx serves. The stack assumes Cloudflare in *Full (strict)*
|
||||||
|
mode in front of it, so a Cloudflare Origin Certificate is enough; any
|
||||||
|
certificate valid for the five hostnames works.
|
||||||
|
3. Point `example.com`, `www`, `api`, `auth`, `stream` and `s3` at the server,
|
||||||
|
proxied/TLS-terminated the same way.
|
||||||
|
|
||||||
|
Then, same two commands as development, with the prod env file and overlay:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
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 up -d
|
||||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
|
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
|
||||||
```
|
```
|
||||||
|
|
||||||
The first command starts Musify's bootstrap jobs (`zitadel-init` and
|
The first command starts the infrastructure, the nginx edge and the bootstrap
|
||||||
`seaweedfs-init` reach Zitadel/SeaweedFS through the public `auth.`/`s3.`
|
jobs; the second builds and starts the applications, already wired to the
|
||||||
URLs, so the edge has to be serving them by then); the second builds and
|
OIDC client ids the first one produced. Both are idempotent — run them again
|
||||||
starts the applications, already wired to the OIDC client ids the first one
|
to deploy a new version (`docker compose ... --profile apps up -d --build`
|
||||||
produced. Both are idempotent, so running them again deploys a new version
|
alone is enough for an update once the stack already exists; it reuses the
|
||||||
(`... --profile apps up -d --build` alone is enough for an update once the
|
infra containers and only rebuilds and restarts the apps).
|
||||||
stack already exists).
|
|
||||||
|
|
||||||
Nothing in this stack publishes a port; everything is reachable only on the
|
Only nginx publishes ports (80/443); everything else is reachable only on the
|
||||||
internal `infra-net` network, through the edge.
|
internal compose network.
|
||||||
|
|
||||||
### Continuous deployment
|
|
||||||
|
|
||||||
`.gitea/workflows/ci-cd.yml` runs the backend and web-player checks on every
|
|
||||||
push/PR, and on a successful push to `master` deploys automatically. The
|
|
||||||
deploy job runs on `gitea-runner`, the self-hosted Gitea Actions runner on
|
|
||||||
the production host (`~/gitea/docker-compose.yml`), configured by
|
|
||||||
`~/gitea/runner-data/config.yaml` (`CONFIG_FILE`): job containers are pinned
|
|
||||||
to the `gitea_gitea` network so `actions/checkout` can resolve `gitea`,
|
|
||||||
`ubuntu-latest` is `node:22` (JS actions need Node inside the job
|
|
||||||
container), and a `production:host` label runs steps natively.
|
|
||||||
|
|
||||||
Native mode means the label's steps execute inside the runner container
|
|
||||||
itself rather than a fresh one. That container has `/var/run/docker.sock`
|
|
||||||
mounted, so `docker`/`docker compose` there still control the host's real
|
|
||||||
Docker daemon (sibling containers, "Docker outside of Docker"). Its compose
|
|
||||||
service also bind-mounts `/home/ubuntu/deploys` into itself at the *same*
|
|
||||||
path, which matters because compose's own relative bind mounts (`./keys`)
|
|
||||||
are resolved by the host daemon against that literal path; mounting it
|
|
||||||
anywhere else would silently bind the wrong (empty) directory.
|
|
||||||
|
|
||||||
`DEPLOY_PATH` (hardcoded in the workflow as `/home/ubuntu/deploys/musify`)
|
|
||||||
is a persistent clone on that host, reused instead of a fresh checkout each
|
|
||||||
run since `deploy/.env.prod` is untracked and only lives there.
|
|
||||||
`/home/ubuntu/deploys/SharedServices` is the sibling clone of the
|
|
||||||
`Infrastructure` repo (shared services + `edge/`), already running; its
|
|
||||||
`.env.prod` shares the Postgres/RabbitMQ/S3 credentials with Musify's own
|
|
||||||
`deploy/.env.prod` by design (see that repo's README). The edge is deployed
|
|
||||||
from that repo, not from this pipeline.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -187,12 +142,17 @@ from that repo, not from this pipeline.
|
|||||||
|
|
||||||
```
|
```
|
||||||
deploy/
|
deploy/
|
||||||
├─ compose.yml # Musify's own services: wait-for-postgres, keys-init,
|
├─ compose.yml # all services, incl. keys-init / migrate / zitadel-init
|
||||||
│ # migrate, seaweedfs-init, zitadel-init, api/worker/gateway/web-player
|
├─ compose.dev.yml # dev overlay: host ports, pgAdmin
|
||||||
├─ compose.dev.yml # dev overlay: host ports
|
├─ compose.prod.yml # prod overlay: nginx edge
|
||||||
├─ compose.prod.yml # prod overlay: joins infra-net
|
|
||||||
├─ .env.example / .env.prod.example
|
├─ .env.example / .env.prod.example
|
||||||
└─ keys/ # RS256 stream-ticket key pair (generated)
|
├─ keys/ # RS256 stream-ticket key pair (generated)
|
||||||
|
├─ nginx/
|
||||||
|
│ ├─ templates/default.conf.template # vhosts, rendered with the domain by envsubst
|
||||||
|
│ ├─ snippets/proxy.conf # proxy headers shared by every vhost
|
||||||
|
│ └─ certs/ # origin.pem + origin.key (not versioned)
|
||||||
|
└─ zitadel/
|
||||||
|
└─ .output/ # PAT written by Zitadel on init (not versioned)
|
||||||
```
|
```
|
||||||
|
|
||||||
The application images are built from `backend/Musify.*.Dockerfile` (build
|
The application images are built from `backend/Musify.*.Dockerfile` (build
|
||||||
@@ -201,12 +161,18 @@ and `web-player/Dockerfile`.
|
|||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- **S3 credentials, the Postgres admin user, the RabbitMQ user and the
|
- **S3 credentials** are rendered into the SeaweedFS identity file from
|
||||||
Zitadel masterkey** all live in Infrastructure's own `.env`/`.env.prod` now.
|
`S3_ACCESS_KEY` / `S3_SECRET_KEY` when the container starts, so they cannot
|
||||||
This project's env files only duplicate the values that have to match (see
|
drift from the ones the apps are given.
|
||||||
the comments in `.env.example`).
|
|
||||||
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||||
works on an empty volume and on an existing one.
|
works on an empty volume and on an existing one.
|
||||||
- **Re-running `zitadel-init`, `seaweedfs-init` or `migrate`** is safe: they
|
- **`latest` Zitadel images** require the standalone Login UI v2 container,
|
||||||
reuse the existing project/apps/bucket by name, and `migrate` is a normal
|
which this stack does not run. `zitadel-init` turns that requirement off so
|
||||||
`dotnet ef database update`, a no-op once the schema is current.
|
the built-in `/ui/login` is used.
|
||||||
|
- **`AppSettings*.json` are PascalCase** in `Musify.Api` and `Musify.Worker`;
|
||||||
|
on case-sensitive Linux the host looks for `appsettings*.json`, so their
|
||||||
|
Dockerfiles create lowercase copies at publish time.
|
||||||
|
- **Re-running `zitadel-init` or `migrate`** is safe: the former reuses the
|
||||||
|
existing project/apps by name (regenerating the web app's secret only if it
|
||||||
|
is missing from the env file), the latter is a normal `dotnet ef database
|
||||||
|
update`, a no-op once the schema is current.
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user