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
|
||||
.vs/
|
||||
artifacts/
|
||||
TestResults/
|
||||
*.trx
|
||||
*.coverage
|
||||
|
||||
# Rider / VS
|
||||
.idea/
|
||||
@@ -28,4 +25,5 @@ Thumbs.db
|
||||
# Deploy secrets and runtime state
|
||||
deploy/keys/
|
||||
deploy/secrets/
|
||||
deploy/nginx/certs/
|
||||
deploy/zitadel/.output/
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
# Musify
|
||||
|
||||
A music streaming app. Upload your own tracks, organize them into playlists
|
||||
and albums, and stream them back with a web player built around your
|
||||
listening habits: auto-generated mixes, a home feed, search, likes, and an
|
||||
Explore page for finding something new.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Upload and organize your own music**: tracks, albums, cover art, artist credits.
|
||||
- **Playlists**, likes, and a "recently played" history.
|
||||
- **Mixes**, playlists generated automatically from what you actually listen to.
|
||||
- **Explore**, genre browsing and search across tracks, albums, playlists and people.
|
||||
- **Real streaming**, not just file downloads: seek, queue, shuffle, repeat.
|
||||
- **Accounts via OIDC login**, with an optional anonymous listening mode.
|
||||
- Light and dark themes, and a layout that works on both desktop and mobile.
|
||||
|
||||
## How it's built
|
||||
|
||||
**Backend**: .NET 10, split into a REST API, a background worker, and a
|
||||
dedicated streaming gateway.
|
||||
- Clean architecture (Domain / Application / Infrastructure) with CQRS (Mediator) and `ErrorOr` results.
|
||||
- PostgreSQL via EF Core for metadata. RabbitMQ and MassTransit handle async processing (audio transcoding, thumbnails) with saga-based rollback.
|
||||
- SeaweedFS (S3-compatible) for file storage. Uploads and downloads go straight between the client and storage, never through the API.
|
||||
- ffmpeg for audio transcoding, ImageSharp for thumbnails.
|
||||
- A YARP reverse proxy that streams the audio and checks a signed, short-lived ticket on every request.
|
||||
- Zitadel for login (OIDC/OAuth2).
|
||||
|
||||
**Web client**: SvelteKit 2 on Svelte 5 (runes), TypeScript, Tailwind 4.
|
||||
The session lives on the server, so the backend access token never reaches the browser.
|
||||
|
||||
**Infrastructure**: Musify's own apps run from Docker Compose (dev and
|
||||
production overlays). Postgres, RabbitMQ, SeaweedFS, Zitadel and Jaeger are
|
||||
shared infrastructure that lives in a separate `Infrastructure` git
|
||||
repository, used by Musify and by other projects that need the same OIDC
|
||||
provider and object storage.
|
||||
|
||||
## Project layout
|
||||
Music streaming platform: upload your own tracks, process them asynchronously
|
||||
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
|
||||
bytes ever going through the API.
|
||||
|
||||
```
|
||||
backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
|
||||
web-player/ the SvelteKit web client
|
||||
deploy/ Docker Compose stacks for dev and production
|
||||
docs/ architecture notes, one file per area
|
||||
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
|
||||
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
||||
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
|
||||
docs/ What each piece does and why (in Spanish)
|
||||
```
|
||||
|
||||
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
|
||||
layout. They live in the separate `Infrastructure` repository instead.
|
||||
```sh
|
||||
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,
|
||||
authentication, storage, streaming, media processing, and the local dev
|
||||
setup. [deploy/README.md](deploy/README.md) has the full instructions for
|
||||
running the stack, in development or on a production server.
|
||||
Development (Docker Desktop):
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
||||
```
|
||||
|
||||
Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC
|
||||
provisioning — all as one-shot jobs baked into the compose file. Apps run from
|
||||
the IDE by default; add `--profile apps up -d --build` as a second command to
|
||||
run them in Docker instead.
|
||||
|
||||
Production, on a server with a domain:
|
||||
|
||||
```sh
|
||||
cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
|
||||
```
|
||||
|
||||
Idempotent — run the same commands again to deploy an update. See
|
||||
[deploy/README.md](deploy/README.md) for the details, the port map and the
|
||||
production prerequisites.
|
||||
|
||||
## Components
|
||||
|
||||
| Service | Port (dev) | Role |
|
||||
|---|---|---|
|
||||
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
|
||||
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
|
||||
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails |
|
||||
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
|
||||
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
|
||||
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
|
||||
|
||||
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
|
||||
[authentication](docs/authentication.md), [storage](docs/storage.md),
|
||||
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
|
||||
[development](docs/development.md).
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Musify.Application.Albums.Responses
|
||||
{
|
||||
|
||||
/// <summary>A page of the user's catalog albums (their own).</summary>
|
||||
public record AlbumsSearchResponse(
|
||||
IReadOnlyList<AlbumSearchItemResponse> Items,
|
||||
int PageNumber,
|
||||
|
||||
@@ -3,7 +3,8 @@ using Musify.Application.Configuration.Responses;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
|
||||
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
|
||||
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
|
||||
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
||||
|
||||
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
||||
|
||||
@@ -2,16 +2,28 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
|
||||
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
|
||||
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
|
||||
public sealed class PlaybackConfiguration
|
||||
{
|
||||
public const string SectionName = "Playback";
|
||||
|
||||
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
|
||||
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
|
||||
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
|
||||
public bool AllowAnonymousListening { get; set; }
|
||||
|
||||
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
|
||||
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
|
||||
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
|
||||
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
|
||||
[Range(0, 3600)]
|
||||
public int AnonymousFragmentSeconds { get; set; }
|
||||
|
||||
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
|
||||
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
|
||||
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
|
||||
/// ever changes, since the two aren't wired together.</summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
namespace Musify.Application.Configuration.Responses
|
||||
{
|
||||
|
||||
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
|
||||
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
|
||||
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
|
||||
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ namespace Musify.Application.Contracts
|
||||
{
|
||||
public interface IStreamTicketService
|
||||
{
|
||||
|
||||
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
|
||||
/// <c>sub</c> claim.</param>
|
||||
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
|
||||
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
|
||||
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
|
||||
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Musify.Application.Mixes
|
||||
public record GenerateMixesForUserCommand(long UserId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
|
||||
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
|
||||
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
|
||||
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
|
||||
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
|
||||
public class GenerateMixesForUserCommandHandler(
|
||||
IDatabase database,
|
||||
MixConfiguration mixConfiguration,
|
||||
|
||||
@@ -5,7 +5,9 @@ using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Mixes
|
||||
{
|
||||
|
||||
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
|
||||
/// track lookup, instead of an EF Include chain deep enough (Mix -> Items -> Track -> Owner) to
|
||||
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
|
||||
internal static class MixItemMapper
|
||||
{
|
||||
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
||||
|
||||
@@ -4,6 +4,13 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musify.Application.Serialization;
|
||||
|
||||
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
|
||||
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
|
||||
// them to the nearest representable double, corrupting the id (it becomes a
|
||||
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
|
||||
// on the specific id-shaped `long`/`long?` response properties (not globally —
|
||||
// unrelated `long` fields like byte sizes must stay numbers), this serializes
|
||||
// them as a JSON string instead, which round-trips exactly.
|
||||
public sealed class LongAsStringConverter : JsonConverter<long>
|
||||
{
|
||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
|
||||
@@ -30,6 +30,8 @@ public sealed class TrackStreamIssuer(
|
||||
folderPath,
|
||||
streamGatewayConfiguration.AudioFileName);
|
||||
|
||||
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
|
||||
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
|
||||
if (userId != null)
|
||||
{
|
||||
var newListeningHistory = new ListeningHistory
|
||||
|
||||
@@ -9,7 +9,8 @@ using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
|
||||
/// <param name="UserId">Null for an anonymous request — allowed only when
|
||||
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
|
||||
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
||||
: IQuery<ErrorOr<TrackStreamResponse>>;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Musify.Application.Tracks.Responses
|
||||
{
|
||||
|
||||
/// <summary>A page of the user's catalog tracks (their own uploads).</summary>
|
||||
public record TracksSearchResponse(
|
||||
IReadOnlyList<TrackSearchItemResponse> Items,
|
||||
int PageNumber,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace Musify.Domain.ValueObjects
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
|
||||
/// be tagged as both Classical and Metal at the same time).
|
||||
/// </summary>
|
||||
public static class GenreCompatibility
|
||||
{
|
||||
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
||||
@@ -28,6 +31,9 @@ namespace Musify.Domain.ValueObjects
|
||||
public static bool AreCompatible(Genre a, Genre b) =>
|
||||
a == b || !IncompatiblePairs.Contains((a, b));
|
||||
|
||||
/// <summary>
|
||||
/// Returns every mutually exclusive genre pair found within the given set of tags.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
||||
{
|
||||
var distinctTags = tags.Distinct().ToList();
|
||||
|
||||
@@ -24,6 +24,11 @@ namespace Musify.Infrastructure.Persistence
|
||||
if (context == null)
|
||||
return;
|
||||
|
||||
// SavingChanges(Async) fires before EF's own automatic change detection for this
|
||||
// save, so entities modified through plain property assignment (query, then set a
|
||||
// property, then SaveChanges — the normal case) still show as Unchanged here unless
|
||||
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
|
||||
// never bumped on a real update, only on inserts.
|
||||
context.ChangeTracker.DetectChanges();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -37,7 +42,7 @@ namespace Musify.Infrastructure.Persistence
|
||||
}
|
||||
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)
|
||||
: DbContext, AppIDatabase
|
||||
{
|
||||
private static readonly AuditableEntityInterceptor AuditInterceptor = new();
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder
|
||||
.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"))
|
||||
.AddInterceptors(AuditInterceptor);
|
||||
optionsBuilder.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"));
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
|
||||
@@ -18,7 +18,10 @@ namespace Musify.Infrastructure.Persistence
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
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>());
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
||||
partial class AddUserProfilePictureUrl
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserProfilePictureUrl : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
||||
partial class RenameTimestampsAndAudit
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RenameTimestampsAndAudit : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "CreatedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
||||
partial class AddTrackProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
||||
partial class AddTrackProcessingSagaKeys
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackProcessingSagaKeys : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -29,6 +29,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
||||
partial class AddPlayListProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddPlayListProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260713234847_AddListeningHistory")]
|
||||
partial class AddListeningHistory
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddListeningHistory : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -47,6 +47,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260714114537_AddTrackDuration")]
|
||||
partial class AddTrackDuration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackDuration : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
||||
partial class AddTrackSourceArtistExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackSourceArtistExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
filter: "\"ExternalId\" <> ''");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260715230836_AddTrackDownloadRequested")]
|
||||
partial class AddTrackDownloadRequested
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackDownloadRequested : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
||||
partial class MakeTrackMediaAndExternalIdNullable
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -66,6 +66,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
||||
partial class AddArtistsAndTrackOwnership
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddArtistsAndTrackOwnership : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -154,6 +154,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716220047_DropTrackArtistColumn")]
|
||||
partial class DropTrackArtistColumn
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DropTrackArtistColumn : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Tracks");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
||||
partial class MovePlayListPicturesToOwnedType
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,15 +4,16 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MovePlayListPicturesToOwnedType : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
||||
partial class RenameTrackDurationToDurationSeconds
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RenameTrackDurationToDurationSeconds : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "DurationSeconds");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
||||
partial class SplitTrackIntoLocalAndExternal
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class SplitTrackIntoLocalAndExternal : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
@@ -156,6 +156,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
||||
partial class MakePlayListPicturesNullable
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListPicturesNullable : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -51,6 +51,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
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 Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720180629_WidenTrackTitle")]
|
||||
partial class WidenTrackTitle
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class WidenTrackTitle : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 50);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720180747_AddAlbums")]
|
||||
partial class AddAlbums
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbums : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -100,6 +100,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720184656_AddUserAlbumExternalId")]
|
||||
partial class AddUserAlbumExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserAlbumExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721155102_AddMixes")]
|
||||
partial class AddMixes
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddMixes : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -83,6 +83,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "TrackId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
||||
partial class RemoveUserAlbumExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUserAlbumExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -24,6 +24,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721162102_DropMixGeneratedOn")]
|
||||
partial class DropMixGeneratedOn
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DropMixGeneratedOn : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Mixes");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
||||
partial class MakePlayListDescriptionOptional
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListDescriptionOptional : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724170051_DurationAsDouble")]
|
||||
partial class DurationAsDouble
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,15 +4,16 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DurationAsDouble : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724181012_AddExternalAlbums")]
|
||||
partial class AddExternalAlbums
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddExternalAlbums : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -36,6 +36,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
||||
partial class AddExternalAlbumThumbnail
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddExternalAlbumThumbnail : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
|
||||
partial class AddAlbumProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbumProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914144913_AddPlayListVisibility")]
|
||||
partial class AddPlayListVisibility
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddPlayListVisibility : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914150045_AddTrackLikes")]
|
||||
partial class AddTrackLikes
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackLikes : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914151900_AddUserFollows")]
|
||||
partial class AddUserFollows
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserFollows : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260915165436_RemoveYouTubeIntegration")]
|
||||
partial class RemoveYouTubeIntegration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+13
-3
@@ -5,16 +5,25 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveYouTubeIntegration : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
// Data cleanup first, while the External*/Local*/UserAlbums tables (and the loose
|
||||
// MixItem columns) still exist to query/cascade from. Deleting the base Tracks/Albums
|
||||
// rows for anything that only ever existed as an ExternalTrack/ExternalAlbum cascades
|
||||
// (FKs are ON DELETE CASCADE) to their own ExternalTracks/ExternalAlbums row, TrackArtist,
|
||||
// PlayListHasTrack, AlbumHasTrack, TrackLike, ListeningHistory, UserHasTrack and MixItems
|
||||
// rows that referenced them — this is deliberately lossy: those rows only existed to back
|
||||
// the YouTube integration being removed here.
|
||||
migrationBuilder.Sql("""DELETE FROM "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
|
||||
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
|
||||
|
||||
// Live YouTube search hits that were seeded into a mix but never materialized into a
|
||||
// real track (MixItemSource.YouTube, no TrackId) — MixItem.TrackId is about to become
|
||||
// required, so these rows can't be kept.
|
||||
migrationBuilder.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -123,6 +132,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260916153700_AddTrackTags")]
|
||||
partial class AddTrackTags
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackTags : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
|
||||
@@ -41,8 +41,13 @@ public sealed class JwtBearerEventsHandler(
|
||||
if (cache.TryGetValue(cacheKey, out _))
|
||||
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);
|
||||
|
||||
// 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
|
||||
?? userInfo?.PreferredUsername
|
||||
?? principal.FindFirstValue("name")
|
||||
|
||||
@@ -39,6 +39,13 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
|
||||
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, _) =>
|
||||
{
|
||||
if (context.JsonPropertyInfo?.CustomConverter is LongAsStringConverter or NullableLongAsStringConverter)
|
||||
|
||||
@@ -48,4 +48,6 @@ app
|
||||
|
||||
app.Run();
|
||||
|
||||
// Makes the top-level-statements Program class public so
|
||||
// WebApplicationFactory<Program> (Musify.Api.Tests) can see it.
|
||||
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;
|
||||
|
||||
/// <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 static readonly TicketValidationResult Invalid = new(string.Empty, null);
|
||||
|
||||
@@ -72,6 +72,12 @@ public sealed class TicketValidationMiddleware(
|
||||
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)
|
||||
{
|
||||
var rangeHeader = context.Request.Headers.Range;
|
||||
|
||||
@@ -7,6 +7,11 @@ using Musify.Infrastructure.Services;
|
||||
|
||||
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
|
||||
{
|
||||
ValidateOnBuild = false,
|
||||
|
||||
@@ -12,6 +12,10 @@ RUN dotnet restore Hosts/Musify.Api/Api.csproj
|
||||
|
||||
COPY . .
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
@@ -11,6 +11,9 @@ RUN dotnet restore Hosts/Musify.Worker/Worker.csproj
|
||||
|
||||
COPY . .
|
||||
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
|
||||
WORKDIR /app
|
||||
|
||||
-6
@@ -1,4 +1,3 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Domain.Entities;
|
||||
using Musify.Infrastructure.Tests.TestSupport;
|
||||
using Xunit;
|
||||
@@ -44,11 +43,6 @@ namespace Musify.Infrastructure.Tests.Persistence
|
||||
Assert.Equal(originalCreatedAt, user.CreatedAt);
|
||||
Assert.True(user.UpdatedAt > originalCreatedAt,
|
||||
$"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.
|
||||
# Copy to deploy/.env and adjust.
|
||||
# Copy to deploy/.env and adjust. Not versioned.
|
||||
#
|
||||
# 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
|
||||
|
||||
# --- Public URLs -------------------------------------------------------------
|
||||
# How each service is reached from *outside* Docker. `host.docker.internal`
|
||||
# resolves to the same address from the browser, from apps running on the
|
||||
# host and from inside the containers, which is what makes the OIDC issuer
|
||||
# and the presigned S3 URLs valid in both run modes.
|
||||
# resolves to the same address from the browser, from apps running on the host
|
||||
# and from inside the containers, which is what makes the OIDC issuer and the
|
||||
# presigned S3 URLs valid in both run modes.
|
||||
PUBLIC_WEB_URL=http://localhost:3000
|
||||
PUBLIC_API_URL=http://localhost:5111
|
||||
PUBLIC_STREAM_URL=http://localhost:8081
|
||||
PUBLIC_AUTH_URL=http://host.docker.internal:8080
|
||||
|
||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.
|
||||
POSTGRES_HOST=host.docker.internal
|
||||
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=musify_db
|
||||
POSTGRES_HOST_PORT=59000
|
||||
|
||||
# --- Zitadel (OIDC, shared instance) -----------------------------------------
|
||||
# Path to the admin service-account PAT Infrastructure's Zitadel writes on
|
||||
# init. Adjust if your Infrastructure checkout lives somewhere else relative
|
||||
# to this one.
|
||||
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
|
||||
# Allows the OIDC apps this project creates to use plain-http redirect URIs
|
||||
# (dev only).
|
||||
# --- Zitadel (OIDC) ----------------------------------------------------------
|
||||
# The masterkey MUST be exactly 32 characters. The external domain/port form
|
||||
# the token issuer, so they have to match PUBLIC_AUTH_URL. Changing them needs
|
||||
# a re-init: docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down -v
|
||||
ZITADEL_MASTERKEY=MasterkeyNeedsToHave32Characters
|
||||
ZITADEL_EXTERNAL_DOMAIN=host.docker.internal
|
||||
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
|
||||
|
||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||
@@ -42,25 +42,17 @@ AUTH_CLIENT_ID=
|
||||
WEB_CLIENT_ID=
|
||||
WEB_CLIENT_SECRET=
|
||||
|
||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
||||
# Must match Infrastructure's .env.
|
||||
RABBITMQ_HOST=host.docker.internal
|
||||
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||
RABBITMQ_DEFAULT_USER=guest
|
||||
RABBITMQ_DEFAULT_PASS=guest
|
||||
|
||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
||||
# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env.
|
||||
SEAWEEDFS_HOST=host.docker.internal
|
||||
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||
S3_ACCESS_KEY=admin_access_key
|
||||
S3_SECRET_KEY=admin_secret_key
|
||||
S3_BUCKET=webapi-storage
|
||||
S3_PUBLIC_URL=http://host.docker.internal:8333
|
||||
S3_INTERNAL_URL=http://host.docker.internal:8333
|
||||
S3_USE_HTTP=true
|
||||
|
||||
# --- Jaeger (shared instance, tracing) ---------------------------------------
|
||||
JAEGER_HOST=host.docker.internal
|
||||
|
||||
# --- Web player --------------------------------------------------------------
|
||||
# Key material for the encrypted session cookie: any long random string.
|
||||
SESSION_SECRET=dev-session-secret-change-me-0123456789abcdef
|
||||
@@ -69,3 +61,6 @@ AUTH_ALLOW_INSECURE_HTTP=true
|
||||
|
||||
# --- Misc --------------------------------------------------------------------
|
||||
AUTH_REQUIRE_HTTPS=false
|
||||
PGADMIN_DEFAULT_EMAIL=admin@musify.dev
|
||||
PGADMIN_DEFAULT_PASSWORD=admin
|
||||
PGADMIN_HOST_PORT=5050
|
||||
|
||||
+27
-31
@@ -1,41 +1,41 @@
|
||||
# Production configuration for deploy/compose.yml + compose.prod.yml.
|
||||
# Copy to deploy/.env.prod, then:
|
||||
#
|
||||
# 1. replace every `*.example.com` with your actual domains (each service
|
||||
# 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),
|
||||
# 1. replace every `example.com` with your domain,
|
||||
# 2. replace every CHANGE_ME with a strong random value
|
||||
# (`openssl rand -hex 24`),
|
||||
# 3. point each domain below at the server and add its vhost to the shared
|
||||
# edge (Infrastructure's edge/conf.d).
|
||||
# (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32 chars),
|
||||
# 3. point these DNS records at the server, all proxied/TLS-terminated the
|
||||
# same way: example.com, www, api, auth, stream, s3.
|
||||
#
|
||||
# Requires ../../../Infrastructure running first, on the same Docker host,
|
||||
# with its own `infra-net` network already created. See
|
||||
# ../../../Infrastructure/README.md. Not versioned. Keep it readable only by
|
||||
# the deploy user (chmod 600).
|
||||
# Not versioned. Keep it readable only by the deploy user (chmod 600).
|
||||
|
||||
DOTNET_ENVIRONMENT=Production
|
||||
|
||||
# --- Domains -------------------------------------------------------------
|
||||
# Public URLs. They must match the vhosts of the shared edge nginx
|
||||
# (Infrastructure's edge/conf.d), which is what serves them; the domains are
|
||||
# independent, they don't need to share a root.
|
||||
PUBLIC_WEB_URL=https://musify.example.com
|
||||
PUBLIC_API_URL=https://musifyapi.example.com
|
||||
PUBLIC_STREAM_URL=https://musifystream.example.com
|
||||
# --- Domain ------------------------------------------------------------------
|
||||
# ROOT_DOMAIN drives the nginx vhosts; the URLs below must agree with it.
|
||||
ROOT_DOMAIN=example.com
|
||||
PUBLIC_WEB_URL=https://example.com
|
||||
PUBLIC_API_URL=https://api.example.com
|
||||
PUBLIC_STREAM_URL=https://stream.example.com
|
||||
PUBLIC_AUTH_URL=https://auth.example.com
|
||||
S3_PUBLIC_URL=https://s3.example.com
|
||||
|
||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.prod.
|
||||
POSTGRES_HOST=postgres
|
||||
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=musify_db
|
||||
|
||||
# --- Zitadel (OIDC, shared instance) -----------------------------------------
|
||||
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
|
||||
# --- Zitadel (OIDC) ----------------------------------------------------------
|
||||
# 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
|
||||
|
||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||
@@ -44,22 +44,16 @@ AUTH_CLIENT_ID=
|
||||
WEB_CLIENT_ID=
|
||||
WEB_CLIENT_SECRET=
|
||||
|
||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||
RABBITMQ_DEFAULT_USER=musify
|
||||
RABBITMQ_DEFAULT_PASS=CHANGE_ME
|
||||
|
||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
||||
SEAWEEDFS_HOST=seaweedfs
|
||||
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||
S3_ACCESS_KEY=CHANGE_ME
|
||||
S3_SECRET_KEY=CHANGE_ME
|
||||
S3_BUCKET=webapi-storage
|
||||
S3_INTERNAL_URL=http://seaweedfs:8333
|
||||
S3_USE_HTTP=false
|
||||
|
||||
# --- Jaeger (shared instance, tracing) ---------------------------------------
|
||||
JAEGER_HOST=jaeger
|
||||
|
||||
# --- Web player --------------------------------------------------------------
|
||||
# Key material for the encrypted session cookie. Rotating it logs everyone out.
|
||||
SESSION_SECRET=CHANGE_ME
|
||||
@@ -67,3 +61,5 @@ AUTH_ALLOW_INSECURE_HTTP=false
|
||||
|
||||
# --- Misc --------------------------------------------------------------------
|
||||
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
|
||||
|
||||
Everything needed to run Musify itself, in development and in production,
|
||||
from the same set of compose files, no wrapper scripts, just `docker
|
||||
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.
|
||||
Everything needed to run Musify, in development and in production, from the
|
||||
same set of compose files — no wrapper scripts, just `docker compose`.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `compose.yml` | Musify's own services, 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.prod.yml` | Production overlay: joins Infrastructure's `infra-net` network. |
|
||||
| `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 the ports to the host, adds pgAdmin. |
|
||||
| `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`. |
|
||||
|
||||
The four applications (`api`, `worker`, `gateway`, `web-player`) sit behind
|
||||
the `apps` compose profile, so a plain `up` starts only the one-shot
|
||||
bootstrap jobs, the usual development setup, with the apps running from the
|
||||
IDE.
|
||||
The four applications (`api`, `worker`, `gateway`, `web-player`) sit behind the
|
||||
`apps` compose profile, so a plain `up` starts infrastructure only — the usual
|
||||
development setup, with the apps running from the IDE.
|
||||
|
||||
### 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:
|
||||
|
||||
| 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. |
|
||||
| `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 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. |
|
||||
| `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. |
|
||||
|
||||
`api` and `worker` wait for `migrate`; `api` waits for `keys-init`
|
||||
(`depends_on: condition: service_completed_successfully`). Nothing waits on
|
||||
`zitadel-init`, `seaweedfs-init` or the shared infrastructure services
|
||||
directly. Compose interpolates `${AUTH_CLIENT_ID}` once, when a command
|
||||
starts, so a container that started in the *same* `up` as `zitadel-init`
|
||||
would still get the empty value it had before provisioning ran. That's why
|
||||
bringing up the apps is a second command: by the time you run it,
|
||||
`api` and `worker` wait for `migrate`; `api` and `gateway` wait for
|
||||
`keys-init` (`depends_on: condition: service_completed_successfully`). They do
|
||||
**not** wait for `zitadel-init` — compose interpolates `${AUTH_CLIENT_ID}` once,
|
||||
when a command starts, so a container that starts in the *same* `up` as
|
||||
`zitadel-init` would still get the empty value it had before provisioning ran.
|
||||
That's why 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.
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
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
|
||||
```
|
||||
|
||||
The first command starts Musify's own bootstrap jobs (Postgres wait, keys,
|
||||
migrations, the bucket, the Zitadel project/apps). The second builds and runs
|
||||
`api`, `worker`, `gateway` and `web-player` in Docker too. Skip it to run the
|
||||
apps from the IDE instead (the usual dev loop):
|
||||
The first command starts the infrastructure and runs the bootstrap jobs
|
||||
(keys, migrations, Zitadel project/apps). The second builds and runs `api`,
|
||||
`worker`, `gateway` and `web-player` in Docker too — skip it to run the apps
|
||||
from the IDE instead (the usual dev loop):
|
||||
|
||||
```sh
|
||||
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:*`
|
||||
from user-secrets and `web-player` reads its own `web-player/.env`, neither
|
||||
looks at `deploy/.env`. Copy the four values `zitadel-init` wrote
|
||||
(`AUTH_CLIENT_ID`, `WEB_CLIENT_ID`, `WEB_CLIENT_SECRET`, `NATIVE_CLIENT_ID`)
|
||||
into `dotnet user-secrets set Authentication:ClientId <value> --project
|
||||
backend/Hosts/Musify.Api` and into `web-player/.env` once. They stay valid
|
||||
until Infrastructure's Zitadel volume is wiped.
|
||||
from user-secrets and `web-player` reads its own `web-player/.env` — neither
|
||||
looks at `deploy/.env`. Copy the three values `zitadel-init` wrote
|
||||
(`AUTH_CLIENT_ID`, `WEB_CLIENT_ID`, `WEB_CLIENT_SECRET`) into
|
||||
`dotnet user-secrets set Authentication:ClientId <value> --project
|
||||
backend/Hosts/Musify.Api` and into `web-player/.env` once; they stay valid
|
||||
until you wipe the Zitadel volume.
|
||||
|
||||
Add `--profile tools` to also start pgAdmin.
|
||||
|
||||
### Ports
|
||||
|
||||
| Service | Port | Used by |
|
||||
|---|---|---|
|
||||
| Musify.Api | `5111` | `/scalar/v1`, `/openapi/v1.json` |
|
||||
| Musify.StreamingGateway | `8081` | `/health`, `/media/...` |
|
||||
| web-player | `5173` (`npm run dev`) or `3000` (container) | |
|
||||
| PostgreSQL | `59000` → 5432 | API + Worker (EF Core) and Zitadel |
|
||||
| Zitadel | `8080` | OIDC/OAuth2, console at `/ui/console` |
|
||||
| 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
|
||||
Infrastructure. See its own README for that table.
|
||||
|
||||
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.
|
||||
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`
|
||||
|
||||
`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
|
||||
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
|
||||
address Infrastructure's own dev ports are published on.
|
||||
presigned S3 URLs valid whichever way the apps are running. Zitadel routes by
|
||||
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
|
||||
|
||||
```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 -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
|
||||
|
||||
Musify doesn't run its own nginx: the shared edge in Infrastructure
|
||||
(`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:
|
||||
On a server with a domain, from the repository root:
|
||||
|
||||
```sh
|
||||
cp deploy/.env.prod.example deploy/.env.prod
|
||||
```
|
||||
|
||||
Replace every `*.example.com` with your domains (they must match the
|
||||
edge's vhosts) and every `CHANGE_ME` with a random value
|
||||
(`openssl rand -hex 24`). `chmod 600 deploy/.env.prod`.
|
||||
Fill it in:
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
The first command starts Musify's bootstrap jobs (`zitadel-init` and
|
||||
`seaweedfs-init` reach Zitadel/SeaweedFS through the public `auth.`/`s3.`
|
||||
URLs, so the edge has to be serving them by then); the second builds and
|
||||
starts the applications, already wired to the OIDC client ids the first one
|
||||
produced. Both are idempotent, so running them again deploys a new version
|
||||
(`... --profile apps up -d --build` alone is enough for an update once the
|
||||
stack already exists).
|
||||
The first command starts the infrastructure, the nginx edge and the bootstrap
|
||||
jobs; the second builds and starts the applications, already wired to the
|
||||
OIDC client ids the first one produced. Both are idempotent — run them again
|
||||
to deploy a new version (`docker compose ... --profile apps up -d --build`
|
||||
alone is enough for an update once the stack already exists; it reuses the
|
||||
infra containers and only rebuilds and restarts the apps).
|
||||
|
||||
Nothing in this stack publishes a port; everything is reachable only on the
|
||||
internal `infra-net` network, through the edge.
|
||||
|
||||
### 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.
|
||||
Only nginx publishes ports (80/443); everything else is reachable only on the
|
||||
internal compose network.
|
||||
|
||||
---
|
||||
|
||||
@@ -187,12 +142,17 @@ from that repo, not from this pipeline.
|
||||
|
||||
```
|
||||
deploy/
|
||||
├─ compose.yml # Musify's own services: wait-for-postgres, keys-init,
|
||||
│ # migrate, seaweedfs-init, zitadel-init, api/worker/gateway/web-player
|
||||
├─ compose.dev.yml # dev overlay: host ports
|
||||
├─ compose.prod.yml # prod overlay: joins infra-net
|
||||
├─ compose.yml # all services, incl. keys-init / migrate / zitadel-init
|
||||
├─ compose.dev.yml # dev overlay: host ports, pgAdmin
|
||||
├─ compose.prod.yml # prod overlay: nginx edge
|
||||
├─ .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
|
||||
@@ -201,12 +161,18 @@ and `web-player/Dockerfile`.
|
||||
|
||||
## Notes
|
||||
|
||||
- **S3 credentials, the Postgres admin user, the RabbitMQ user and the
|
||||
Zitadel masterkey** all live in Infrastructure's own `.env`/`.env.prod` now.
|
||||
This project's env files only duplicate the values that have to match (see
|
||||
the comments in `.env.example`).
|
||||
- **S3 credentials** are rendered into the SeaweedFS identity file from
|
||||
`S3_ACCESS_KEY` / `S3_SECRET_KEY` when the container starts, so they cannot
|
||||
drift from the ones the apps are given.
|
||||
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||
works on an empty volume and on an existing one.
|
||||
- **Re-running `zitadel-init`, `seaweedfs-init` or `migrate`** is safe: they
|
||||
reuse the existing project/apps/bucket by name, and `migrate` is a normal
|
||||
`dotnet ef database update`, a no-op once the schema is current.
|
||||
- **`latest` Zitadel images** require the standalone Login UI v2 container,
|
||||
which this stack does not run. `zitadel-init` turns that requirement off so
|
||||
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