Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d892d976a3 | ||
|
|
c07676f486 | ||
|
|
e4b0ce3df0 | ||
|
|
1a2bafe96c | ||
|
|
85f06edc53 | ||
|
|
a0ba57b569 | ||
|
|
8c6d4d6a6d | ||
|
|
137a3b62bb | ||
|
|
9fd0bf46d6 | ||
|
|
e187016c70 | ||
|
|
89326c0a16 | ||
|
|
65518e72e3 | ||
|
|
521cafc5c4 | ||
|
|
3e56725566 | ||
|
|
fbca8156a7 | ||
|
|
4b3d0b5fbd | ||
|
|
ec5b84f748 |
@@ -1,115 +0,0 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
|
||||
concurrency:
|
||||
group: production-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
backend-tests:
|
||||
name: BackEnd
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
defaults:
|
||||
run:
|
||||
working-directory: backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
|
||||
- name: Restore
|
||||
run: dotnet restore Musify.slnx
|
||||
|
||||
- name: Build
|
||||
run: dotnet build Musify.slnx --no-restore -c Release
|
||||
|
||||
- name: Test
|
||||
run: dotnet test Musify.slnx --no-build -c Release
|
||||
|
||||
web-player-checks:
|
||||
name: WebPlayer
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
defaults:
|
||||
run:
|
||||
working-directory: web-player
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: web-player/package-lock.json
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Type-check
|
||||
run: npm run check
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
# Runs on the production host itself, through the existing self-hosted
|
||||
# Gitea Actions runner (act_runner), which also carries a "production"
|
||||
# label registered in native (non-docker) mode so its steps see the host's
|
||||
# real Docker daemon directly (Docker socket + a bind-mounted deploy
|
||||
# directory — see deploy/README.md for how that runner is set up).
|
||||
#
|
||||
# DEPLOY_PATH is the persistent clone already on that host (the one whose
|
||||
# deploy/.env.prod is filled in and matches SharedServices/.env.prod) —
|
||||
# deploy reuses it instead of a fresh checkout, since .env.prod is
|
||||
# untracked and only lives there.
|
||||
deploy:
|
||||
name: Deploy
|
||||
needs: [backend-tests, web-player-checks]
|
||||
if: github.ref == 'refs/heads/master' && github.event_name == 'push'
|
||||
runs-on: [production]
|
||||
timeout-minutes: 20
|
||||
env:
|
||||
DEPLOY_PATH: /home/ubuntu/deploys/musify
|
||||
COMPOSE: docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml
|
||||
steps:
|
||||
- name: Pull latest master
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config --global --add safe.directory "$DEPLOY_PATH"
|
||||
cd "$DEPLOY_PATH"
|
||||
git fetch origin master
|
||||
git reset --hard origin/master
|
||||
|
||||
- name: Migrate database, generate keys
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$DEPLOY_PATH"
|
||||
$COMPOSE up -d wait-for-postgres keys-init migrate
|
||||
|
||||
# Both jobs verify themselves through the public auth./s3. URLs, served
|
||||
# by the shared edge nginx (Infrastructure's edge/), and are idempotent.
|
||||
- name: Create OIDC app and S3 bucket
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$DEPLOY_PATH"
|
||||
$COMPOSE up -d seaweedfs-init zitadel-init
|
||||
for job in musify-seaweedfs-init musify-zitadel-init; do
|
||||
code=$(docker wait "$job")
|
||||
if [ "$code" != "0" ]; then
|
||||
docker logs "$job"
|
||||
echo "$job exited with $code"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Build and start apps
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$DEPLOY_PATH"
|
||||
$COMPOSE --profile apps up -d --build api worker gateway web-player
|
||||
+1
-3
@@ -7,9 +7,6 @@ obj/
|
||||
*.userosscache
|
||||
.vs/
|
||||
artifacts/
|
||||
TestResults/
|
||||
*.trx
|
||||
*.coverage
|
||||
|
||||
# Rider / VS
|
||||
.idea/
|
||||
@@ -28,4 +25,5 @@ Thumbs.db
|
||||
# Deploy secrets and runtime state
|
||||
deploy/keys/
|
||||
deploy/secrets/
|
||||
deploy/nginx/certs/
|
||||
deploy/zitadel/.output/
|
||||
|
||||
@@ -1,55 +1,57 @@
|
||||
# Musify
|
||||
|
||||
A music streaming app. Upload your own tracks, organize them into playlists
|
||||
and albums, and stream them back with a web player built around your
|
||||
listening habits: auto-generated mixes, a home feed, search, likes, and an
|
||||
Explore page for finding something new.
|
||||
|
||||
## What it does
|
||||
|
||||
- **Upload and organize your own music**: tracks, albums, cover art, artist credits.
|
||||
- **Playlists**, likes, and a "recently played" history.
|
||||
- **Mixes**, playlists generated automatically from what you actually listen to.
|
||||
- **Explore**, genre browsing and search across tracks, albums, playlists and people.
|
||||
- **Real streaming**, not just file downloads: seek, queue, shuffle, repeat.
|
||||
- **Accounts via OIDC login**, with an optional anonymous listening mode.
|
||||
- Light and dark themes, and a layout that works on both desktop and mobile.
|
||||
|
||||
## How it's built
|
||||
|
||||
**Backend**: .NET 10, split into a REST API, a background worker, and a
|
||||
dedicated streaming gateway.
|
||||
- Clean architecture (Domain / Application / Infrastructure) with CQRS (Mediator) and `ErrorOr` results.
|
||||
- PostgreSQL via EF Core for metadata. RabbitMQ and MassTransit handle async processing (audio transcoding, thumbnails) with saga-based rollback.
|
||||
- SeaweedFS (S3-compatible) for file storage. Uploads and downloads go straight between the client and storage, never through the API.
|
||||
- ffmpeg for audio transcoding, ImageSharp for thumbnails.
|
||||
- A YARP reverse proxy that streams the audio and checks a signed, short-lived ticket on every request.
|
||||
- Zitadel for login (OIDC/OAuth2).
|
||||
|
||||
**Web client**: SvelteKit 2 on Svelte 5 (runes), TypeScript, Tailwind 4.
|
||||
The session lives on the server, so the backend access token never reaches the browser.
|
||||
|
||||
**Infrastructure**: Musify's own apps run from Docker Compose (dev and
|
||||
production overlays). Postgres, RabbitMQ, SeaweedFS, Zitadel and Jaeger are
|
||||
shared infrastructure that lives in a separate `Infrastructure` git
|
||||
repository, used by Musify and by other projects that need the same OIDC
|
||||
provider and object storage.
|
||||
|
||||
## Project layout
|
||||
Music streaming platform: upload your own tracks, process them asynchronously
|
||||
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
|
||||
bytes ever going through the API.
|
||||
|
||||
```
|
||||
backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
|
||||
web-player/ the SvelteKit web client
|
||||
deploy/ Docker Compose stacks for dev and production
|
||||
docs/ architecture notes, one file per area
|
||||
backend/ .NET 10 — Domain / Application / Infrastructure / Api / Worker / StreamingGateway / Tests
|
||||
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
||||
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
|
||||
docs/ What each piece does and why (in Spanish)
|
||||
```
|
||||
|
||||
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
|
||||
layout. They live in the separate `Infrastructure` repository instead.
|
||||
```sh
|
||||
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
|
||||
```
|
||||
|
||||
## Digging deeper
|
||||
## Run it
|
||||
|
||||
[docs/](docs/README.md) covers how each piece works and why: architecture,
|
||||
authentication, storage, streaming, media processing, and the local dev
|
||||
setup. [deploy/README.md](deploy/README.md) has the full instructions for
|
||||
running the stack, in development or on a production server.
|
||||
Development (Docker Desktop):
|
||||
|
||||
```sh
|
||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
||||
```
|
||||
|
||||
Infrastructure plus stream-ticket keys, database migrations and Zitadel OIDC
|
||||
provisioning — all as one-shot jobs baked into the compose file. Apps run from
|
||||
the IDE by default; add `--profile apps up -d --build` as a second command to
|
||||
run them in Docker instead.
|
||||
|
||||
Production, on a server with a domain:
|
||||
|
||||
```sh
|
||||
cp deploy/.env.prod.example deploy/.env.prod # fill in the domain and secrets
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml up -d
|
||||
docker compose --env-file deploy/.env.prod -f deploy/compose.yml -f deploy/compose.prod.yml --profile apps up -d --build
|
||||
```
|
||||
|
||||
Idempotent — run the same commands again to deploy an update. See
|
||||
[deploy/README.md](deploy/README.md) for the details, the port map and the
|
||||
production prerequisites.
|
||||
|
||||
## Components
|
||||
|
||||
| Service | Port (dev) | Role |
|
||||
|---|---|---|
|
||||
| `Musify.Api` | 5111 | REST API, issues presigned upload URLs and stream tickets |
|
||||
| `Musify.StreamingGateway` | 8081 | YARP proxy that validates the stream ticket and serves the audio |
|
||||
| `Musify.Worker` | — | MassTransit consumers: transcoding, thumbnails |
|
||||
| `web-player` | 5173 / 3000 | Web client (SvelteKit, server-side OIDC session) |
|
||||
| Zitadel | 8080 | Identity (OIDC/OAuth2) |
|
||||
| PostgreSQL / RabbitMQ / SeaweedFS / Jaeger | 59000 / 5672 / 8333 / 16686 | Metadata, messaging, object storage, traces |
|
||||
|
||||
More in [docs/](docs/README.md): [architecture](docs/architecture.md),
|
||||
[authentication](docs/authentication.md), [storage](docs/storage.md),
|
||||
[streaming](docs/streaming.md), [media processing](docs/media-processing.md),
|
||||
[development](docs/development.md).
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Musify.Application.Albums
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
||||
.Select(albumTrack => new { albumTrack.Track, ListensCount = albumTrack.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(albumTrack => new { albumTrack.Track, ListensCount = albumTrack.Track.ListeningHistories.Count })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -16,7 +16,7 @@ namespace Musify.Application.Albums
|
||||
{
|
||||
var listenedTrackIds = database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(history => history.UserId == request.UserId && history.IsCounted);
|
||||
.Where(history => history.UserId == request.UserId);
|
||||
|
||||
var ranked = await database.AlbumHasTracks
|
||||
.AsNoTracking()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace Musify.Application.Albums.Responses
|
||||
{
|
||||
|
||||
/// <summary>A page of the user's catalog albums (their own).</summary>
|
||||
public record AlbumsSearchResponse(
|
||||
IReadOnlyList<AlbumSearchItemResponse> Items,
|
||||
int PageNumber,
|
||||
|
||||
@@ -3,7 +3,8 @@ using Musify.Application.Configuration.Responses;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
|
||||
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
|
||||
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
|
||||
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
||||
|
||||
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
||||
|
||||
@@ -2,16 +2,28 @@ using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Musify.Application.Configuration
|
||||
{
|
||||
|
||||
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
|
||||
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
|
||||
public sealed class PlaybackConfiguration
|
||||
{
|
||||
public const string SectionName = "Playback";
|
||||
|
||||
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
|
||||
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
|
||||
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
|
||||
public bool AllowAnonymousListening { get; set; }
|
||||
|
||||
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
|
||||
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
|
||||
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
|
||||
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
|
||||
[Range(0, 3600)]
|
||||
public int AnonymousFragmentSeconds { get; set; }
|
||||
|
||||
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
|
||||
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
|
||||
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
|
||||
/// ever changes, since the two aren't wired together.</summary>
|
||||
[Range(1, int.MaxValue)]
|
||||
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
namespace Musify.Application.Configuration.Responses
|
||||
{
|
||||
|
||||
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
|
||||
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
|
||||
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
|
||||
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ namespace Musify.Application.Contracts
|
||||
{
|
||||
public interface IStreamTicketService
|
||||
{
|
||||
|
||||
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
|
||||
/// <c>sub</c> claim.</param>
|
||||
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
|
||||
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
|
||||
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
|
||||
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ namespace Musify.Application.Likes
|
||||
|
||||
var pagedEntities = await likesQuery
|
||||
.OrderByDescending(like => like.CreatedAt)
|
||||
.Select(like => new { like.Track, ListensCount = like.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(like => new { like.Track, ListensCount = like.Track.ListeningHistories.Count })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -12,6 +12,11 @@ namespace Musify.Application.Mixes
|
||||
public record GenerateMixesForUserCommand(long UserId)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
|
||||
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
|
||||
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
|
||||
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
|
||||
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
|
||||
public class GenerateMixesForUserCommandHandler(
|
||||
IDatabase database,
|
||||
MixConfiguration mixConfiguration,
|
||||
@@ -38,7 +43,7 @@ namespace Musify.Application.Mixes
|
||||
|
||||
var historyTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(history => history.UserId == userId && history.IsCounted)
|
||||
.Where(history => history.UserId == userId)
|
||||
.Select(history => history.TrackId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -5,7 +5,9 @@ using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.Mixes
|
||||
{
|
||||
|
||||
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
|
||||
/// track lookup, instead of an EF Include chain deep enough (Mix -> Items -> Track -> Owner) to
|
||||
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
|
||||
internal static class MixItemMapper
|
||||
{
|
||||
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
||||
@@ -19,7 +21,7 @@ namespace Musify.Application.Mixes
|
||||
.AsNoTracking()
|
||||
.Include(track => track.Owner)
|
||||
.Where(track => trackIds.Contains(track.Id))
|
||||
.Select(track => new { Track = track, ListensCount = track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(track => new { Track = track, ListensCount = track.ListeningHistories.Count })
|
||||
.ToDictionaryAsync(x => x.Track.Id, cancellationToken);
|
||||
|
||||
return orderedItems
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace Musify.Application.PlayLists
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderBy(plt => plt.Position)
|
||||
.Select(plt => new { plt.Track, ListensCount = plt.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(plt => new { plt.Track, ListensCount = plt.Track.ListeningHistories.Count })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -4,6 +4,13 @@ using System.Text.Json.Serialization;
|
||||
|
||||
namespace Musify.Application.Serialization;
|
||||
|
||||
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
|
||||
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
|
||||
// them to the nearest representable double, corrupting the id (it becomes a
|
||||
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
|
||||
// on the specific id-shaped `long`/`long?` response properties (not globally —
|
||||
// unrelated `long` fields like byte sizes must stay numbers), this serializes
|
||||
// them as a JSON string instead, which round-trips exactly.
|
||||
public sealed class LongAsStringConverter : JsonConverter<long>
|
||||
{
|
||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||
|
||||
@@ -30,7 +30,8 @@ public sealed class TrackStreamIssuer(
|
||||
folderPath,
|
||||
streamGatewayConfiguration.AudioFileName);
|
||||
|
||||
Guid? listenId = null;
|
||||
// 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
|
||||
@@ -40,10 +41,9 @@ public sealed class TrackStreamIssuer(
|
||||
};
|
||||
await database.ListeningHistories.AddAsync(newListeningHistory, cancellationToken);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
listenId = newListeningHistory.Id;
|
||||
}
|
||||
|
||||
return new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds, listenId);
|
||||
return new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds);
|
||||
}
|
||||
|
||||
private long? AnonymousFragmentBytes(long? userId)
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record DeleteStaleUncountedListensCommand : ICommand<int>;
|
||||
|
||||
public class DeleteStaleUncountedListensCommandHandler(
|
||||
IDatabase database,
|
||||
ILogger<DeleteStaleUncountedListensCommandHandler> logger)
|
||||
: ICommandHandler<DeleteStaleUncountedListensCommand, int>
|
||||
{
|
||||
public async ValueTask<int> Handle(DeleteStaleUncountedListensCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var cutoff = DateTime.UtcNow.AddDays(-ListeningRules.UncountedRetentionDays);
|
||||
|
||||
var stale = await database.ListeningHistories
|
||||
.Where(l => !l.IsCounted && l.ListenedAt < cutoff)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (stale.Count == 0)
|
||||
return 0;
|
||||
|
||||
database.ListeningHistories.RemoveRange(stale);
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
logger.LogInformation("Deleted {Count} uncounted listens older than {Days} days", stale.Count, ListeningRules.UncountedRetentionDays);
|
||||
return stale.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Track.Tags)
|
||||
.Where(t => t.UserId == query.UserId)
|
||||
.OrderByDescending(t => t.ListenedAt)
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count(l => l.IsCounted)))
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
return track == null
|
||||
|
||||
@@ -17,7 +17,7 @@ namespace Musify.Application.Tracks
|
||||
{
|
||||
var recentTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == query.UserId && l.IsCounted)
|
||||
.Where(l => l.UserId == query.UserId)
|
||||
.GroupBy(l => l.TrackId)
|
||||
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
||||
.Select(g => g.Key)
|
||||
@@ -29,7 +29,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => recentTrackIds.Contains(t.Id))
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count })
|
||||
.ToDictionaryAsync(t => t.Id, cancellationToken);
|
||||
|
||||
return recentTrackIds
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace Musify.Application.Tracks
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => t.Id == request.TrackId)
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
|
||||
@@ -9,7 +9,8 @@ using Musify.Application.Tracks.Responses;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
|
||||
/// <param name="UserId">Null for an anonymous request — allowed only when
|
||||
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
|
||||
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
||||
: IQuery<ErrorOr<TrackStreamResponse>>;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace Musify.Application.Tracks
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderByDescending(t => t.Track.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var items = pagedEntities
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace Musify.Application.Tracks
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderBy(t => t.Track.CreatedAt)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
|
||||
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public static class ListeningRules
|
||||
{
|
||||
public const double CountedThresholdSeconds = 30;
|
||||
public const double CountedThresholdFraction = 0.5;
|
||||
public const double DurationToleranceSeconds = 2;
|
||||
public const double ClockToleranceSeconds = 5;
|
||||
public const int UncountedRetentionDays = 8;
|
||||
|
||||
public static double CountedThreshold(double durationSeconds) =>
|
||||
Math.Min(CountedThresholdSeconds, durationSeconds * CountedThresholdFraction);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record RecordListeningProgressCommand(
|
||||
long UserId,
|
||||
Guid ListenId,
|
||||
double PlayedSeconds)
|
||||
: ICommand<ErrorOr<Success>>;
|
||||
|
||||
public class RecordListeningProgressCommandHandler(IDatabase database)
|
||||
: ICommandHandler<RecordListeningProgressCommand, ErrorOr<Success>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<Success>> Handle(RecordListeningProgressCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!double.IsFinite(request.PlayedSeconds) || request.PlayedSeconds < 0)
|
||||
return Error.Validation(description: "playedSeconds must be a non-negative number.");
|
||||
|
||||
var listen = await database.ListeningHistories
|
||||
.Include(l => l.Track)
|
||||
.SingleOrDefaultAsync(l => l.Id == request.ListenId && l.UserId == request.UserId, cancellationToken);
|
||||
|
||||
if (listen == null)
|
||||
return Error.NotFound();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var durationCap = listen.Track.DurationSeconds + ListeningRules.DurationToleranceSeconds;
|
||||
var clockCap = (now - listen.ListenedAt).TotalSeconds + ListeningRules.ClockToleranceSeconds;
|
||||
var accepted = Math.Min(request.PlayedSeconds, Math.Min(durationCap, clockCap));
|
||||
|
||||
listen.PlayedSeconds = Math.Max(listen.PlayedSeconds ?? 0, accepted);
|
||||
listen.LastProgressAt = now;
|
||||
listen.IsCounted = listen.IsCounted
|
||||
|| listen.PlayedSeconds >= ListeningRules.CountedThreshold(listen.Track.DurationSeconds);
|
||||
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return Result.Success;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,5 @@ namespace Musify.Application.Tracks.Responses
|
||||
public record TrackStreamResponse(
|
||||
string ManifestUrl,
|
||||
string Ticket,
|
||||
int ExpiresInSeconds,
|
||||
Guid? ListenId = null);
|
||||
int ExpiresInSeconds);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -20,18 +20,17 @@ namespace Musify.Application.Users
|
||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||
|
||||
var tracksThisWeek = await thisWeekHistory
|
||||
.Where(l => l.IsCounted)
|
||||
.Select(l => l.TrackId)
|
||||
.Distinct()
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var secondsThisWeek = await thisWeekHistory
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
|
||||
.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 && l.IsCounted)
|
||||
.Where(l => l.UserId == request.UserId)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -19,12 +19,6 @@ namespace Musify.Domain.Entities
|
||||
[Required]
|
||||
public DateTime ListenedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public double? PlayedSeconds { get; set; }
|
||||
|
||||
public DateTime? LastProgressAt { get; set; }
|
||||
|
||||
public bool IsCounted { get; set; }
|
||||
|
||||
[ForeignKey(nameof(UserId) )]
|
||||
public User User { get; set; }
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
namespace Musify.Domain.ValueObjects
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
|
||||
/// be tagged as both Classical and Metal at the same time).
|
||||
/// </summary>
|
||||
public static class GenreCompatibility
|
||||
{
|
||||
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
||||
@@ -28,6 +31,9 @@ namespace Musify.Domain.ValueObjects
|
||||
public static bool AreCompatible(Genre a, Genre b) =>
|
||||
a == b || !IncompatiblePairs.Contains((a, b));
|
||||
|
||||
/// <summary>
|
||||
/// Returns every mutually exclusive genre pair found within the given set of tags.
|
||||
/// </summary>
|
||||
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
||||
{
|
||||
var distinctTags = tags.Distinct().ToList();
|
||||
|
||||
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Jobs
|
||||
{
|
||||
var userIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(history => history.IsCounted)
|
||||
.Select(history => history.UserId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
using Mediator;
|
||||
using Musify.Application.Tracks;
|
||||
|
||||
namespace Musify.Infrastructure.Jobs
|
||||
{
|
||||
public class ListeningHistoryCleanupJob(IMediator mediator)
|
||||
{
|
||||
public async Task RunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await mediator.Send(new DeleteStaleUncountedListensCommand(), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,11 @@ namespace Musify.Infrastructure.Persistence
|
||||
if (context == null)
|
||||
return;
|
||||
|
||||
// SavingChanges(Async) fires before EF's own automatic change detection for this
|
||||
// save, so entities modified through plain property assignment (query, then set a
|
||||
// property, then SaveChanges — the normal case) still show as Unchanged here unless
|
||||
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
|
||||
// never bumped on a real update, only on inserts.
|
||||
context.ChangeTracker.DetectChanges();
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
@@ -37,7 +42,7 @@ namespace Musify.Infrastructure.Persistence
|
||||
}
|
||||
else if (entry.State == EntityState.Modified)
|
||||
{
|
||||
entry.Property(nameof(IAuditable.UpdatedAt)).CurrentValue = now;
|
||||
entry.Entity.UpdatedAt = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,9 @@ namespace Musify.Infrastructure.Persistence
|
||||
public class Database(DatabaseConfiguration configuration)
|
||||
: DbContext, AppIDatabase
|
||||
{
|
||||
private static readonly AuditableEntityInterceptor AuditInterceptor = new();
|
||||
|
||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||
{
|
||||
optionsBuilder
|
||||
.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"))
|
||||
.AddInterceptors(AuditInterceptor);
|
||||
optionsBuilder.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"));
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
|
||||
@@ -18,7 +18,10 @@ namespace Musify.Infrastructure.Persistence
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddDbContext<Database>();
|
||||
serviceDescriptors.AddSingleton<AuditableEntityInterceptor>();
|
||||
|
||||
serviceDescriptors.AddDbContext<Database>((serviceProvider, options) =>
|
||||
options.AddInterceptors(serviceProvider.GetRequiredService<AuditableEntityInterceptor>()));
|
||||
|
||||
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
||||
partial class AddUserProfilePictureUrl
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserProfilePictureUrl : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
||||
partial class RenameTimestampsAndAudit
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RenameTimestampsAndAudit : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "CreatedAt");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
||||
partial class AddTrackProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
||||
partial class AddTrackProcessingSagaKeys
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackProcessingSagaKeys : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -29,6 +29,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
||||
partial class AddPlayListProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddPlayListProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260713234847_AddListeningHistory")]
|
||||
partial class AddListeningHistory
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddListeningHistory : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -47,6 +47,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "UserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260714114537_AddTrackDuration")]
|
||||
partial class AddTrackDuration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackDuration : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
||||
partial class AddTrackSourceArtistExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackSourceArtistExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
filter: "\"ExternalId\" <> ''");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260715230836_AddTrackDownloadRequested")]
|
||||
partial class AddTrackDownloadRequested
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackDownloadRequested : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
||||
partial class MakeTrackMediaAndExternalIdNullable
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -66,6 +66,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
||||
partial class AddArtistsAndTrackOwnership
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddArtistsAndTrackOwnership : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -154,6 +154,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260716220047_DropTrackArtistColumn")]
|
||||
partial class DropTrackArtistColumn
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DropTrackArtistColumn : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Tracks");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
||||
partial class MovePlayListPicturesToOwnedType
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,15 +4,16 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MovePlayListPicturesToOwnedType : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
||||
partial class RenameTrackDurationToDurationSeconds
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RenameTrackDurationToDurationSeconds : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
newName: "DurationSeconds");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
||||
partial class SplitTrackIntoLocalAndExternal
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class SplitTrackIntoLocalAndExternal : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
@@ -156,6 +156,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
||||
partial class MakePlayListPicturesNullable
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListPicturesNullable : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -51,6 +51,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 64);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
backend/Core/Musify.Infrastructure/Persistence/Migrations/20260720180629_WidenTrackTitle.Designer.cs
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720180629_WidenTrackTitle")]
|
||||
partial class WidenTrackTitle
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class WidenTrackTitle : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -31,6 +31,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
oldMaxLength: 50);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720180747_AddAlbums")]
|
||||
partial class AddAlbums
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbums : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -100,6 +100,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260720184656_AddUserAlbumExternalId")]
|
||||
partial class AddUserAlbumExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserAlbumExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721155102_AddMixes")]
|
||||
partial class AddMixes
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddMixes : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -83,6 +83,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "TrackId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
||||
partial class RemoveUserAlbumExternalId
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveUserAlbumExternalId : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
@@ -24,6 +24,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
column: "OwnerUserId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropIndex(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721162102_DropMixGeneratedOn")]
|
||||
partial class DropMixGeneratedOn
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DropMixGeneratedOn : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
table: "Mixes");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<DateOnly>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
||||
partial class MakePlayListDescriptionOptional
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListDescriptionOptional : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
@@ -27,6 +27,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724170051_DurationAsDouble")]
|
||||
partial class DurationAsDouble
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,15 +4,16 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DurationAsDouble : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724181012_AddExternalAlbums")]
|
||||
partial class AddExternalAlbums
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddExternalAlbums : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -36,6 +36,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
||||
partial class AddExternalAlbumThumbnail
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddExternalAlbumThumbnail : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
|
||||
partial class AddAlbumProcessingStateSaga
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbumProcessingStateSaga : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -28,6 +28,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914144913_AddPlayListVisibility")]
|
||||
partial class AddPlayListVisibility
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -4,10 +4,10 @@
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddPlayListVisibility : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
@@ -18,6 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914150045_AddTrackLikes")]
|
||||
partial class AddTrackLikes
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackLikes : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260914151900_AddUserFollows")]
|
||||
partial class AddUserFollows
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddUserFollows : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
@@ -49,6 +49,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260915165436_RemoveYouTubeIntegration")]
|
||||
partial class RemoveYouTubeIntegration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+13
-3
@@ -5,16 +5,25 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveYouTubeIntegration : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
// Data cleanup first, while the External*/Local*/UserAlbums tables (and the loose
|
||||
// MixItem columns) still exist to query/cascade from. Deleting the base Tracks/Albums
|
||||
// rows for anything that only ever existed as an ExternalTrack/ExternalAlbum cascades
|
||||
// (FKs are ON DELETE CASCADE) to their own ExternalTracks/ExternalAlbums row, TrackArtist,
|
||||
// PlayListHasTrack, AlbumHasTrack, TrackLike, ListeningHistory, UserHasTrack and MixItems
|
||||
// rows that referenced them — this is deliberately lossy: those rows only existed to back
|
||||
// the YouTube integration being removed here.
|
||||
migrationBuilder.Sql("""DELETE FROM "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
|
||||
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
|
||||
|
||||
// Live YouTube search hits that were seeded into a mix but never materialized into a
|
||||
// real track (MixItemSource.YouTube, no TrackId) — MixItem.TrackId is about to become
|
||||
// required, so these rows can't be kept.
|
||||
migrationBuilder.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
@@ -123,6 +132,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
|
||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
||||
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
[Migration("20260916153700_AddTrackTags")]
|
||||
partial class AddTrackTags
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
|
||||
+3
-2
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class AddTrackTags : Migration
|
||||
{
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterDatabase()
|
||||
@@ -41,6 +41,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
|
||||
-1173
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user