Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63018a7c72 | ||
|
|
c5748e9765 | ||
|
|
23e30d96fc | ||
|
|
3f8d517ee3 | ||
|
|
2662abe460 | ||
|
|
1b6d7ecb42 | ||
|
|
b60a87adb5 | ||
|
|
5fa901c2e6 | ||
|
|
8975e43729 | ||
|
|
1bdd7ec9f1 | ||
|
|
964dfc809f | ||
|
|
8cea298b2a | ||
|
|
fb4a8ccc0e | ||
|
|
14ee1dc45d | ||
|
|
1320b12699 | ||
|
|
aa20e70671 | ||
|
|
a4ca1f32d3 | ||
|
|
58eb670fed | ||
|
|
7b1b81c45a | ||
|
|
ea9bf07126 | ||
|
|
11789e78fe | ||
|
|
96c75a32c8 |
@@ -1,57 +1,55 @@
|
||||
# Musify
|
||||
|
||||
Music streaming platform: upload your own tracks, process them asynchronously
|
||||
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
|
||||
bytes ever going through the API.
|
||||
A music streaming app. Upload your own tracks, organize them into playlists
|
||||
and albums, and stream them back with a web player built around your
|
||||
listening habits: auto-generated mixes, a home feed, search, likes, and an
|
||||
Explore page for finding something new.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Upload and organize your own music**: tracks, albums, cover art, artist credits.
|
||||
- **Playlists**, likes, and a "recently played" history.
|
||||
- **Mixes**, playlists generated automatically from what you actually listen to.
|
||||
- **Explore**, genre browsing and search across tracks, albums, playlists and people.
|
||||
- **Real streaming**, not just file downloads: seek, queue, shuffle, repeat.
|
||||
- **Accounts via OIDC login**, with an optional anonymous listening mode.
|
||||
- Light and dark themes, and a layout that works on both desktop and mobile.
|
||||
|
||||
## How it's built
|
||||
|
||||
**Backend**: .NET 10, split into a REST API, a background worker, and a
|
||||
dedicated streaming gateway.
|
||||
- Clean architecture (Domain / Application / Infrastructure) with CQRS (Mediator) and `ErrorOr` results.
|
||||
- PostgreSQL via EF Core for metadata. RabbitMQ and MassTransit handle async processing (audio transcoding, thumbnails) with saga-based rollback.
|
||||
- SeaweedFS (S3-compatible) for file storage. Uploads and downloads go straight between the client and storage, never through the API.
|
||||
- ffmpeg for audio transcoding, ImageSharp for thumbnails.
|
||||
- A YARP reverse proxy that streams the audio and checks a signed, short-lived ticket on every request.
|
||||
- Zitadel for login (OIDC/OAuth2).
|
||||
|
||||
**Web client**: SvelteKit 2 on Svelte 5 (runes), TypeScript, Tailwind 4.
|
||||
The session lives on the server, so the backend access token never reaches the browser.
|
||||
|
||||
**Infrastructure**: Musify's own apps run from Docker Compose (dev and
|
||||
production overlays). Postgres, RabbitMQ, SeaweedFS, Zitadel and Jaeger are
|
||||
shared infrastructure that lives in a separate `Infrastructure` git
|
||||
repository, used by Musify and by other projects that need the same OIDC
|
||||
provider and object storage.
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
|
||||
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
||||
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
|
||||
docs/ What each piece does and why (in Spanish)
|
||||
backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
|
||||
web-player/ the SvelteKit web client
|
||||
deploy/ Docker Compose stacks for dev and production
|
||||
docs/ architecture notes, one file per area
|
||||
```
|
||||
|
||||
```sh
|
||||
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
|
||||
```
|
||||
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
|
||||
layout. They live in the separate `Infrastructure` repository instead.
|
||||
|
||||
## Run it
|
||||
## Digging deeper
|
||||
|
||||
Development (Docker Desktop):
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
||||
```
|
||||
|
||||
Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC
|
||||
provisioning — all as one-shot jobs baked into the compose file. Apps run from
|
||||
the IDE by default; add `--profile apps up -d --build` as a second command to
|
||||
run them in Docker instead.
|
||||
|
||||
Production, on a server with a domain:
|
||||
|
||||
```sh
|
||||
cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
|
||||
```
|
||||
|
||||
Idempotent — run the same commands again to deploy an update. See
|
||||
[deploy/README.md](deploy/README.md) for the details, the port map and the
|
||||
production prerequisites.
|
||||
|
||||
## Components
|
||||
|
||||
| Service | Port (dev) | Role |
|
||||
|---|---|---|
|
||||
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
|
||||
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
|
||||
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails |
|
||||
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
|
||||
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
|
||||
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
|
||||
|
||||
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
|
||||
[authentication](docs/authentication.md), [storage](docs/storage.md),
|
||||
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
|
||||
[development](docs/development.md).
|
||||
[docs/](docs/README.md) covers how each piece works and why: architecture,
|
||||
authentication, storage, streaming, media processing, and the local dev
|
||||
setup. [deploy/README.md](deploy/README.md) has the full instructions for
|
||||
running the stack, in development or on a production server.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Musify.Application.Albums.Responses
|
||||
{
|
||||
/// <summary>A page of the user's catalog albums (their own).</summary>
|
||||
|
||||
public record AlbumsSearchResponse(
|
||||
IReadOnlyList<AlbumSearchItemResponse> Items,
|
||||
int PageNumber,
|
||||
|
||||
@@ -77,8 +77,6 @@ namespace Musify.Application.Albums
|
||||
finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
}
|
||||
|
||||
database.Albums.Update(album);
|
||||
|
||||
if (pictureIntent != null && finalPictureKey != null)
|
||||
{
|
||||
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
|
||||
|
||||
@@ -3,8 +3,7 @@ using Musify.Application.Configuration.Responses;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
|
||||
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
|
||||
|
||||
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
||||
|
||||
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
||||
|
||||
@@ -2,28 +2,16 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
|
||||
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
|
||||
|
||||
public sealed class PlaybackConfiguration
|
||||
{
|
||||
public const string SectionName = "Playback";
|
||||
|
||||
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
|
||||
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
|
||||
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
|
||||
public bool AllowAnonymousListening { get; set; }
|
||||
|
||||
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
|
||||
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
|
||||
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
|
||||
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
|
||||
[Range(0, 3600)]
|
||||
public int AnonymousFragmentSeconds { get; set; }
|
||||
|
||||
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
|
||||
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
|
||||
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
|
||||
/// ever changes, since the two aren't wired together.</summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
||||
}
|
||||
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
namespace Musify.Application.Configuration.Responses
|
||||
{
|
||||
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
|
||||
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
|
||||
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
|
||||
|
||||
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,7 @@ namespace Musify.Application.Contracts
|
||||
{
|
||||
public interface IStreamTicketService
|
||||
{
|
||||
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
|
||||
/// <c>sub</c> claim.</param>
|
||||
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
|
||||
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
|
||||
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
|
||||
|
||||
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,6 @@ namespace Musify.Application.Mixes
|
||||
public record GenerateMixesForUserCommand(long UserId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
|
||||
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
|
||||
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
|
||||
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
|
||||
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
|
||||
public class GenerateMixesForUserCommandHandler(
|
||||
IDatabase database,
|
||||
MixConfiguration mixConfiguration,
|
||||
|
||||
@@ -5,9 +5,7 @@ using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Mixes
|
||||
{
|
||||
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
|
||||
/// track lookup, instead of an EF Include chain deep enough (Mix -> 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(
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace Musify.Application.PlayLists
|
||||
}, validation.Value);
|
||||
}
|
||||
|
||||
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||
private async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
|
||||
@@ -85,8 +85,6 @@ namespace Musify.Application.PlayLists
|
||||
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
}
|
||||
|
||||
database.PlayLists.Update(playListEntity);
|
||||
|
||||
if (pictureIntent != null && finalPictureKey != null)
|
||||
{
|
||||
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
||||
@@ -117,7 +115,7 @@ namespace Musify.Application.PlayLists
|
||||
return PlayListApplicationResponse.FromEntity(playListEntity, coverTrackIds);
|
||||
}
|
||||
|
||||
async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||
private async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
|
||||
@@ -4,13 +4,6 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musify.Application.Serialization;
|
||||
|
||||
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
|
||||
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
|
||||
// them to the nearest representable double, corrupting the id (it becomes a
|
||||
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
|
||||
// on the specific id-shaped `long`/`long?` response properties (not globally —
|
||||
// unrelated `long` fields like byte sizes must stay numbers), this serializes
|
||||
// them as a JSON string instead, which round-trips exactly.
|
||||
public sealed class LongAsStringConverter : JsonConverter<long>
|
||||
{
|
||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
|
||||
@@ -30,8 +30,6 @@ public sealed class TrackStreamIssuer(
|
||||
folderPath,
|
||||
streamGatewayConfiguration.AudioFileName);
|
||||
|
||||
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
|
||||
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
|
||||
if (userId != null)
|
||||
{
|
||||
var newListeningHistory = new ListeningHistory
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Musify.Application.Tracks
|
||||
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Track.Owner)
|
||||
.Include(t => t.Track.Tags)
|
||||
.Where(t => t.UserId == query.UserId)
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Application.Tracks
|
||||
public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var recentTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == query.UserId)
|
||||
.GroupBy(l => l.TrackId)
|
||||
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
||||
@@ -24,6 +25,7 @@ namespace Musify.Application.Tracks
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var tracksById = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => recentTrackIds.Contains(t.Id))
|
||||
|
||||
@@ -9,8 +9,7 @@ using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
/// <param name="UserId">Null for an anonymous request — allowed only when
|
||||
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
|
||||
|
||||
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
||||
: IQuery<ErrorOr<TrackStreamResponse>>;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Application.Users
|
||||
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
var thisWeekHistory = database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||
|
||||
var tracksThisWeek = await thisWeekHistory
|
||||
@@ -24,10 +25,11 @@ namespace Musify.Application.Users
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var secondsThisWeek = await thisWeekHistory
|
||||
.Join(database.Tracks, l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
|
||||
.SumAsync(cancellationToken);
|
||||
|
||||
var listenedDates = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
|
||||
@@ -16,22 +16,22 @@ namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserApplicationResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = database.Users
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(request.UsernameSearch))
|
||||
{
|
||||
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
|
||||
user = user.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||
usersQuery = usersQuery.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||
}
|
||||
|
||||
var totalCount = await user.CountAsync(cancellationToken);
|
||||
var pagedUser = await user
|
||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||
var pagedUsers = await usersQuery
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUser);
|
||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUsers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
namespace Musify.Domain.ValueObjects
|
||||
{
|
||||
/// <summary>
|
||||
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
|
||||
/// be tagged as both Classical and Metal at the same time).
|
||||
/// </summary>
|
||||
|
||||
public static class GenreCompatibility
|
||||
{
|
||||
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
||||
@@ -31,9 +28,6 @@ namespace Musify.Domain.ValueObjects
|
||||
public static bool AreCompatible(Genre a, Genre b) =>
|
||||
a == b || !IncompatiblePairs.Contains((a, b));
|
||||
|
||||
/// <summary>
|
||||
/// Returns every mutually exclusive genre pair found within the given set of tags.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
||||
{
|
||||
var distinctTags = tags.Distinct().ToList();
|
||||
|
||||
@@ -24,11 +24,6 @@ namespace Musify.Infrastructure.Persistence
|
||||
if (context == null)
|
||||
return;
|
||||
|
||||
// SavingChanges(Async) fires before EF's own automatic change detection for this
|
||||
// save, so entities modified through plain property assignment (query, then set a
|
||||
// property, then SaveChanges — the normal case) still show as Unchanged here unless
|
||||
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
|
||||
// never bumped on a real update, only on inserts.
|
||||
context.ChangeTracker.DetectChanges();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddUserProfilePictureUrl : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class RenameTimestampsAndAudit : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "CreatedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackProcessingStateSaga : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackProcessingSagaKeys : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -29,7 +29,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddPlayListProcessingStateSaga : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddListeningHistory : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -47,7 +47,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackDuration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackSourceArtistExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
filter: "\"ExternalId\" <> ''");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackDownloadRequested : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -66,7 +66,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddArtistsAndTrackOwnership : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -154,7 +154,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class DropTrackArtistColumn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Tracks");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,16 +4,15 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class MovePlayListPicturesToOwnedType : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class RenameTrackDurationToDurationSeconds : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -16,7 +16,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "DurationSeconds");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class SplitTrackIntoLocalAndExternal : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
@@ -156,7 +156,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class MakePlayListPicturesNullable : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -51,7 +51,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class WidenTrackTitle : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 50);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddAlbums : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -100,7 +100,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddUserAlbumExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddMixes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -83,7 +83,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "TrackId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class RemoveUserAlbumExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -24,7 +24,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class DropMixGeneratedOn : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Mixes");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class MakePlayListDescriptionOptional : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,16 +4,15 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class DurationAsDouble : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddExternalAlbums : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -36,7 +36,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddExternalAlbumThumbnail : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddAlbumProcessingStateSaga : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+2
-3
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddPlayListVisibility : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackLikes : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddUserFollows : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+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
|
||||
|
||||
+3
-13
@@ -5,25 +5,16 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class RemoveYouTubeIntegration : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
// Data cleanup first, while the External*/Local*/UserAlbums tables (and the loose
|
||||
// MixItem columns) still exist to query/cascade from. Deleting the base Tracks/Albums
|
||||
// rows for anything that only ever existed as an ExternalTrack/ExternalAlbum cascades
|
||||
// (FKs are ON DELETE CASCADE) to their own ExternalTracks/ExternalAlbums row, TrackArtist,
|
||||
// PlayListHasTrack, AlbumHasTrack, TrackLike, ListeningHistory, UserHasTrack and MixItems
|
||||
// rows that referenced them — this is deliberately lossy: those rows only existed to back
|
||||
// the YouTube integration being removed here.
|
||||
|
||||
migrationBuilder.Sql("""DELETE FROM "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
|
||||
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
|
||||
|
||||
// Live YouTube search hits that were seeded into a mix but never materialized into a
|
||||
// real track (MixItemSource.YouTube, no TrackId) — MixItem.TrackId is about to become
|
||||
// required, so these rows can't be kept.
|
||||
migrationBuilder.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -132,7 +123,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
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
|
||||
|
||||
+2
-3
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
public partial class AddTrackTags : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
-1164
File diff suppressed because it is too large
Load Diff
-106
@@ -1,106 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListPictureColumnsOptional : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SmallPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "OriginalPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MediumPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LargePictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SmallPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "OriginalPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MediumPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LargePictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("ProductVersion", "10.0.12")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" });
|
||||
@@ -68,7 +68,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("Delivered");
|
||||
|
||||
b.ToTable("InboxState");
|
||||
b.ToTable("InboxState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||
@@ -159,7 +159,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OutboxMessage");
|
||||
b.ToTable("OutboxMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
|
||||
@@ -189,7 +189,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("Created");
|
||||
|
||||
b.ToTable("OutboxState");
|
||||
b.ToTable("OutboxState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Album", b =>
|
||||
@@ -231,7 +231,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
b.ToTable("Albums", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b =>
|
||||
@@ -259,7 +259,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("AlbumId", "TrackId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AlbumHasTrack");
|
||||
b.ToTable("AlbumHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b =>
|
||||
@@ -283,7 +283,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ListeningHistory");
|
||||
b.ToTable("ListeningHistory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Mix", b =>
|
||||
@@ -317,7 +317,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId", "Position");
|
||||
|
||||
b.ToTable("Mixes");
|
||||
b.ToTable("Mixes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.MixItem", b =>
|
||||
@@ -341,7 +341,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("MixId", "Position");
|
||||
|
||||
b.ToTable("MixItems");
|
||||
b.ToTable("MixItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
|
||||
@@ -385,7 +385,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PlayLists");
|
||||
b.ToTable("PlayLists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
|
||||
@@ -412,7 +412,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("TrackId");
|
||||
|
||||
b.ToTable("PlayListHasTrack");
|
||||
b.ToTable("PlayListHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
|
||||
@@ -450,7 +450,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Tracks");
|
||||
b.ToTable("Tracks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b =>
|
||||
@@ -475,7 +475,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId", "TrackId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TrackLikes");
|
||||
b.ToTable("TrackLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b =>
|
||||
@@ -495,7 +495,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("TrackId", "Tag")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TrackTags");
|
||||
b.ToTable("TrackTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
|
||||
@@ -524,7 +524,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Upload");
|
||||
b.ToTable("Upload", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
|
||||
@@ -575,7 +575,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UploadIntents");
|
||||
b.ToTable("UploadIntents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
|
||||
@@ -613,7 +613,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("User");
|
||||
b.ToTable("User", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b =>
|
||||
@@ -638,7 +638,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("FollowerId", "FollowedId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserFollows");
|
||||
b.ToTable("UserFollows", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
|
||||
@@ -659,7 +659,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserHasTrack");
|
||||
b.ToTable("UserHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b =>
|
||||
@@ -686,7 +686,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("AlbumProcessingState");
|
||||
b.ToTable("AlbumProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b =>
|
||||
@@ -713,7 +713,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("PlayListProcessingState");
|
||||
b.ToTable("PlayListProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
|
||||
@@ -749,7 +749,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("TrackProcessingState");
|
||||
b.ToTable("TrackProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||
@@ -803,7 +803,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("AlbumId");
|
||||
|
||||
b1.ToTable("Albums");
|
||||
b1.ToTable("Albums", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("AlbumId");
|
||||
@@ -896,28 +896,32 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("LargeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("LargePictureName");
|
||||
|
||||
b1.Property<string>("MediumName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("MediumPictureName");
|
||||
|
||||
b1.Property<string>("OriginalName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("OriginalPictureName");
|
||||
|
||||
b1.Property<string>("SmallName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("SmallPictureName");
|
||||
|
||||
b1.HasKey("PlayListId");
|
||||
|
||||
b1.ToTable("PlayLists");
|
||||
b1.ToTable("PlayLists", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayListId");
|
||||
@@ -988,7 +992,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("TrackId");
|
||||
|
||||
b1.ToTable("Tracks");
|
||||
b1.ToTable("Tracks", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TrackId");
|
||||
@@ -1025,7 +1029,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("TrackId");
|
||||
|
||||
b1.ToTable("Tracks");
|
||||
b1.ToTable("Tracks", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TrackId");
|
||||
|
||||
@@ -41,13 +41,8 @@ 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,13 +39,6 @@ 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,6 +48,4 @@ app
|
||||
|
||||
app.Run();
|
||||
|
||||
// Makes the top-level-statements Program class public so
|
||||
// WebApplicationFactory<Program> (Musify.Api.Tests) can see it.
|
||||
public partial class Program;
|
||||
@@ -6,10 +6,6 @@ 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,12 +72,6 @@ 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,11 +7,6 @@ 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,
|
||||
|
||||
@@ -107,5 +107,30 @@ namespace Musify.Application.Tests.PlayLists
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
Assert.Equal("Original description", result.Value.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, playList, intent);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+29
-24
@@ -1,39 +1,39 @@
|
||||
# Development configuration for deploy/compose.yml + compose.dev.yml.
|
||||
# Copy to deploy/.env and adjust. Not versioned.
|
||||
# Copy to deploy/.env and adjust.
|
||||
#
|
||||
# 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 (shared by Musify and Zitadel) -------------------------------
|
||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.
|
||||
POSTGRES_HOST=host.docker.internal
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=musify_db
|
||||
POSTGRES_HOST_PORT=59000
|
||||
|
||||
# --- 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 (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_DEV_MODE=true
|
||||
|
||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||
@@ -42,17 +42,25 @@ AUTH_CLIENT_ID=
|
||||
WEB_CLIENT_ID=
|
||||
WEB_CLIENT_SECRET=
|
||||
|
||||
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
||||
# Must match Infrastructure's .env.
|
||||
RABBITMQ_HOST=host.docker.internal
|
||||
RABBITMQ_DEFAULT_USER=guest
|
||||
RABBITMQ_DEFAULT_PASS=guest
|
||||
|
||||
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
||||
# S3_ACCESS_KEY / S3_SECRET_KEY must match Infrastructure's .env.
|
||||
SEAWEEDFS_HOST=host.docker.internal
|
||||
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
|
||||
@@ -61,6 +69,3 @@ AUTH_ALLOW_INSECURE_HTTP=true
|
||||
|
||||
# --- Misc --------------------------------------------------------------------
|
||||
AUTH_REQUIRE_HTTPS=false
|
||||
PGADMIN_DEFAULT_EMAIL=admin@musify.dev
|
||||
PGADMIN_DEFAULT_PASSWORD=admin
|
||||
PGADMIN_HOST_PORT=5050
|
||||
|
||||
+18
-16
@@ -3,11 +3,14 @@
|
||||
#
|
||||
# 1. replace every `example.com` with your domain,
|
||||
# 2. replace every CHANGE_ME with a strong random value
|
||||
# (`openssl rand -hex 24`; the Zitadel masterkey needs exactly 32 chars),
|
||||
# (`openssl rand -hex 24`),
|
||||
# 3. point these DNS records at the server, all proxied/TLS-terminated the
|
||||
# same way: example.com, www, api, auth, stream, s3.
|
||||
#
|
||||
# Not versioned. Keep it readable only by the deploy user (chmod 600).
|
||||
# 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).
|
||||
|
||||
DOTNET_ENVIRONMENT=Production
|
||||
|
||||
@@ -20,22 +23,15 @@ PUBLIC_STREAM_URL=https://stream.example.com
|
||||
PUBLIC_AUTH_URL=https://auth.example.com
|
||||
S3_PUBLIC_URL=https://s3.example.com
|
||||
|
||||
# --- PostgreSQL (shared by Musify and Zitadel) -------------------------------
|
||||
# --- PostgreSQL (Infrastructure's shared instance, Musify's own database) ---
|
||||
# POSTGRES_USER / POSTGRES_PASSWORD must match Infrastructure's .env.prod.
|
||||
POSTGRES_HOST=postgres
|
||||
POSTGRES_USER=postgres
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
POSTGRES_DB=musify_db
|
||||
|
||||
# --- 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 (OIDC, shared instance) -----------------------------------------
|
||||
ZITADEL_ADMIN_PAT_DIR=../../../Infrastructure/zitadel/.output
|
||||
ZITADEL_DEV_MODE=false
|
||||
|
||||
# OIDC clients: the zitadel-init one-shot service (see compose.yml) fills
|
||||
@@ -44,16 +40,22 @@ AUTH_CLIENT_ID=
|
||||
WEB_CLIENT_ID=
|
||||
WEB_CLIENT_SECRET=
|
||||
|
||||
# --- RabbitMQ (MassTransit) --------------------------------------------------
|
||||
# --- RabbitMQ (shared instance, MassTransit) --------------------------------
|
||||
RABBITMQ_HOST=rabbitmq
|
||||
RABBITMQ_DEFAULT_USER=musify
|
||||
RABBITMQ_DEFAULT_PASS=CHANGE_ME
|
||||
|
||||
# --- SeaweedFS (S3) ----------------------------------------------------------
|
||||
# --- SeaweedFS (S3, shared instance, Musify's own bucket) --------------------
|
||||
SEAWEEDFS_HOST=seaweedfs
|
||||
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
|
||||
|
||||
+86
-76
@@ -1,51 +1,65 @@
|
||||
# Deploy
|
||||
|
||||
Everything needed to run Musify, in development and in production, from the
|
||||
same set of compose files — no wrapper scripts, just `docker compose`.
|
||||
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.
|
||||
|
||||
| File | Role |
|
||||
|---|---|
|
||||
| `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. |
|
||||
| `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: adds the nginx edge and joins Infrastructure's `infra-net` network. |
|
||||
| `.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 infrastructure only — 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 only the one-shot
|
||||
bootstrap jobs, the usual development setup, with the apps running from the
|
||||
IDE.
|
||||
|
||||
### The one-shot bootstrap jobs
|
||||
|
||||
Three services in `compose.yml` run once and exit, so a plain `docker compose
|
||||
Four 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`). |
|
||||
| `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. |
|
||||
| `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. |
|
||||
|
||||
`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,
|
||||
`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,
|
||||
`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 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):
|
||||
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):
|
||||
|
||||
```sh
|
||||
dotnet run --project backend/Hosts/Musify.Api # :5111
|
||||
@@ -55,53 +69,55 @@ 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 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.
|
||||
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.
|
||||
|
||||
### Ports
|
||||
|
||||
| Service | Port | Used by |
|
||||
|---|---|---|
|
||||
| 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` | |
|
||||
| Musify.Api | `5111` | `/scalar/v1`, `/openapi/v1.json` |
|
||||
| Musify.StreamingGateway | `8081` | `/health`, `/media/...` |
|
||||
| web-player | `5173` (`npm run dev`) or `3000` (container) | |
|
||||
|
||||
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.)
|
||||
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.
|
||||
|
||||
### 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. 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).
|
||||
presigned S3 URLs valid whichever way the apps are running, and is the same
|
||||
address Infrastructure's own dev ports are published on.
|
||||
|
||||
### 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 volumes
|
||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml down -v # stop, wipe Musify's own volumes
|
||||
```
|
||||
|
||||
This only touches Musify's own containers and volumes. Resetting Zitadel or
|
||||
SeaweedFS is done from the `Infrastructure` repo instead.
|
||||
|
||||
---
|
||||
|
||||
## Production
|
||||
|
||||
On a server with a domain, from the repository root:
|
||||
On a server with a domain:
|
||||
|
||||
1. Bring up the `Infrastructure` repo first, with its own `.env.prod` filled
|
||||
in. It creates the `infra-net` network this stack joins.
|
||||
2. From this repository's root:
|
||||
|
||||
```sh
|
||||
cp deploy/.env.prod.example deploy/.env.prod
|
||||
@@ -110,31 +126,29 @@ cp deploy/.env.prod.example deploy/.env.prod
|
||||
Fill it in:
|
||||
|
||||
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
|
||||
random value (`openssl rand -hex 24`). `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.
|
||||
proxied and TLS-terminated the same way.
|
||||
|
||||
Then, same two commands as development, with the prod env file and overlay:
|
||||
Then:
|
||||
|
||||
```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 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).
|
||||
The first command starts Musify's bootstrap jobs and the nginx edge; 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).
|
||||
|
||||
Only nginx publishes ports (80/443); everything else is reachable only on the
|
||||
internal compose network.
|
||||
Only nginx publishes ports (80/443). Every other Musify service, and all of
|
||||
Infrastructure's, is reachable only on the internal `infra-net` network.
|
||||
|
||||
---
|
||||
|
||||
@@ -142,17 +156,16 @@ internal compose network.
|
||||
|
||||
```
|
||||
deploy/
|
||||
├─ 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
|
||||
├─ 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: nginx edge, joins infra-net
|
||||
├─ .env.example / .env.prod.example
|
||||
├─ 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)
|
||||
└─ 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)
|
||||
```
|
||||
|
||||
The application images are built from `backend/Musify.*.Dockerfile` (build
|
||||
@@ -161,18 +174,15 @@ and `web-player/Dockerfile`.
|
||||
|
||||
## Notes
|
||||
|
||||
- **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.
|
||||
- **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`).
|
||||
- **Zitadel** runs `start-from-init`, which is idempotent: the same command
|
||||
works on an empty volume and on an existing one.
|
||||
- **`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.
|
||||
- **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.
|
||||
|
||||
+16
-50
@@ -1,67 +1,36 @@
|
||||
name: musify-dev
|
||||
|
||||
# Development overlay: publishes the infrastructure ports to the host so the
|
||||
# .NET apps and the web player can run from the IDE, and adds pgAdmin.
|
||||
# Development overlay: publishes Musify's own ports to the host so the .NET
|
||||
# apps and the web player can run from the IDE.
|
||||
#
|
||||
# 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
|
||||
#
|
||||
# `host.docker.internal` (see ZITADEL_EXTERNAL_DOMAIN / *_PUBLIC_URL in .env)
|
||||
# resolves to the same address from the browser, from apps on the host and from
|
||||
# inside the containers, so the OIDC issuer and the presigned S3 URLs are valid
|
||||
# in both modes. `extra_hosts` gives Linux containers that same name.
|
||||
# Postgres/Zitadel/SeaweedFS/RabbitMQ/Jaeger ports come from Infrastructure's
|
||||
# own compose.dev.yml, not from here. See ../../../Infrastructure/README.md.
|
||||
# `host.docker.internal` resolves to the same address from the browser, from
|
||||
# apps on the host and from inside these containers, which is what makes the
|
||||
# OIDC issuer and the presigned S3 URLs valid in both run modes. `extra_hosts`
|
||||
# gives Linux containers that same name.
|
||||
|
||||
x-host-alias: &host-alias
|
||||
- "${ZITADEL_EXTERNAL_DOMAIN}:host-gateway"
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
ports:
|
||||
- "${POSTGRES_HOST_PORT}:5432"
|
||||
wait-for-postgres:
|
||||
extra_hosts: *host-alias
|
||||
|
||||
zitadel:
|
||||
ports:
|
||||
- "${ZITADEL_EXTERNAL_PORT}:8080"
|
||||
|
||||
rabbitmq:
|
||||
ports:
|
||||
- "5672:5672" # AMQP -> MassTransit
|
||||
- "15672:15672" # management UI
|
||||
|
||||
seaweedfs:
|
||||
ports:
|
||||
- "9333:9333" # master / admin UI
|
||||
- "8888:8888" # filer -> StreamingGateway
|
||||
- "8333:8333" # S3 API -> API, Worker and presigned uploads
|
||||
|
||||
jaeger:
|
||||
ports:
|
||||
- "16686:16686" # UI
|
||||
- "4317:4317" # OTLP gRPC
|
||||
- "4318:4318" # OTLP HTTP
|
||||
seaweedfs-init:
|
||||
extra_hosts: *host-alias
|
||||
|
||||
# zitadel-init (compose.yml) edits whichever env file is mounted here.
|
||||
zitadel-init:
|
||||
extra_hosts: *host-alias
|
||||
volumes:
|
||||
- ./.env:/workspace/.env
|
||||
|
||||
# Optional Postgres UI: docker compose ... --profile tools up -d
|
||||
pgadmin:
|
||||
profiles: [tools]
|
||||
image: dpage/pgadmin4:latest
|
||||
container_name: musify-pgadmin
|
||||
environment:
|
||||
PGADMIN_DEFAULT_EMAIL: ${PGADMIN_DEFAULT_EMAIL}
|
||||
PGADMIN_DEFAULT_PASSWORD: ${PGADMIN_DEFAULT_PASSWORD}
|
||||
PGADMIN_CONFIG_SERVER_MODE: "False"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
ports:
|
||||
- "${PGADMIN_HOST_PORT}:80"
|
||||
volumes:
|
||||
- pgadmin_data:/var/lib/pgadmin
|
||||
restart: unless-stopped
|
||||
migrate:
|
||||
extra_hosts: *host-alias
|
||||
|
||||
api:
|
||||
ports:
|
||||
@@ -80,6 +49,3 @@ services:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
extra_hosts: *host-alias
|
||||
|
||||
volumes:
|
||||
pgadmin_data:
|
||||
|
||||
+12
-12
@@ -1,16 +1,19 @@
|
||||
name: musify-prod
|
||||
|
||||
# Production overlay: adds the nginx edge and the host-mounted secrets.
|
||||
# Nothing but nginx publishes a port; every other service is reachable only on
|
||||
# the internal `musify-prod_default` network.
|
||||
# Production overlay: adds the nginx edge and joins Infrastructure's shared
|
||||
# `infra-net` network so this project's containers can reach Postgres,
|
||||
# Zitadel, SeaweedFS, RabbitMQ and Jaeger by container name. None of them
|
||||
# run as part of this compose project anymore (see
|
||||
# ../../../Infrastructure/README.md, which must already be up). Nothing but
|
||||
# nginx publishes a port; every other service is reachable only on infra-net.
|
||||
#
|
||||
# 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 the infrastructure, the nginx edge and the
|
||||
# one-shot bootstrap jobs (stream keys, migrations, Zitadel project/apps); the
|
||||
# second builds and starts the applications, picking up the OIDC client ids
|
||||
# the first command just wrote into deploy/.env.prod. See deploy/README.md.
|
||||
|
||||
networks:
|
||||
default:
|
||||
name: infra-net
|
||||
external: true
|
||||
|
||||
services:
|
||||
# zitadel-init (compose.yml) edits whichever env file is mounted here.
|
||||
@@ -20,16 +23,13 @@ services:
|
||||
|
||||
# Every vhost proxies through `set $upstream ...; proxy_pass $upstream;`
|
||||
# with the embedded Docker DNS resolver (see nginx/templates), so nginx
|
||||
# does not need api/gateway/web-player to exist at its own startup — only
|
||||
# does not need api/gateway/web-player to exist at its own startup, only
|
||||
# at request time. That is what lets it start in the FIRST command
|
||||
# (profile-gated services aren't part of that invocation yet) and still be
|
||||
# reachable at the auth domain for zitadel-init to provision through.
|
||||
nginx:
|
||||
image: nginx:1.27-alpine
|
||||
container_name: musify-nginx
|
||||
depends_on:
|
||||
zitadel:
|
||||
condition: service_started
|
||||
environment:
|
||||
# The entrypoint renders /etc/nginx/templates/*.template with envsubst.
|
||||
# The filter keeps it from eating nginx's own $host / $remote_addr.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user