Compare commits
109
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302dd1e0f8 | ||
|
|
421fc929e8 | ||
|
|
c4ab5dc388 | ||
|
|
920dcc6779 | ||
|
|
430c711ff2 | ||
|
|
cfdc61a4b8 | ||
|
|
0f28e19c1d | ||
|
|
7316e355ee | ||
|
|
b39e55fd87 | ||
|
|
cb046bba9e | ||
|
|
b9a40186b1 | ||
|
|
fd0a39b879 | ||
|
|
cc7afe4326 | ||
|
|
72cbc18a48 | ||
|
|
84408b842b | ||
|
|
339fcb56ad | ||
|
|
74cf534be5 | ||
|
|
89820c1377 | ||
|
|
2fe26b15dd | ||
|
|
2cb560fda4 | ||
|
|
d8530b9209 | ||
|
|
5049b0ba91 | ||
|
|
1b98a75c0e | ||
|
|
cbbde95773 | ||
|
|
26bb8a50a3 | ||
|
|
bf19bb31f9 | ||
|
|
55cc803708 | ||
|
|
805e521b1d | ||
|
|
f8d431a0d7 | ||
|
|
7b72db8a24 | ||
|
|
b61ff1ebec | ||
|
|
8870c65c6f | ||
|
|
5df5e635f3 | ||
|
|
d5e3cadc43 | ||
|
|
505ae05c53 | ||
|
|
a7929c0ac9 | ||
|
|
4d39008eab | ||
|
|
5efd59826b | ||
|
|
8970f29410 | ||
|
|
523318fc06 | ||
|
|
ba531b4f61 | ||
|
|
3d51f08e0b | ||
|
|
2660c124d5 | ||
|
|
d7199b4d1d | ||
|
|
c60704080b | ||
|
|
1e3e46cfdc | ||
|
|
93d4220f3a | ||
|
|
8a748f2b3a | ||
|
|
2138a4a890 | ||
|
|
94a1981a4c | ||
|
|
2272a401bb | ||
|
|
3ab43a2ef6 | ||
|
|
79698f2265 | ||
|
|
94f80c03c5 | ||
|
|
d98d01ab20 | ||
|
|
6be5c43e6d | ||
|
|
8deb762a52 | ||
|
|
fe21c19810 | ||
|
|
6e86c3d2c5 | ||
|
|
aca4b73fac | ||
|
|
078f7ac6f6 | ||
|
|
b168df6ca6 | ||
|
|
8d178884d4 | ||
|
|
cd2131ef6f | ||
|
|
d6bff15f11 | ||
|
|
b82fca0bc2 | ||
|
|
4fbd4fa81d | ||
|
|
68b5709d88 | ||
|
|
434b2c1305 | ||
|
|
9064ef687c | ||
|
|
c93f3d352c | ||
|
|
a9bfcbbe2f | ||
|
|
717e939038 | ||
|
|
dd1cb8a5d1 | ||
|
|
7c9399a342 | ||
|
|
a3ec9d19d3 | ||
|
|
5f2b5bd5a2 | ||
|
|
3bb4e66ad0 | ||
|
|
fbb102ffbd | ||
|
|
76f7535ffa | ||
|
|
7fcf0645da | ||
|
|
50618865e7 | ||
|
|
fe2f6a785a | ||
|
|
53fc24d4bc | ||
|
|
ff5fa501af | ||
|
|
6e9883018a | ||
|
|
a985ff4fb3 | ||
|
|
63018a7c72 | ||
|
|
c5748e9765 | ||
|
|
23e30d96fc | ||
|
|
3f8d517ee3 | ||
|
|
2662abe460 | ||
|
|
1b6d7ecb42 | ||
|
|
b60a87adb5 | ||
|
|
5fa901c2e6 | ||
|
|
8975e43729 | ||
|
|
1bdd7ec9f1 | ||
|
|
964dfc809f | ||
|
|
8cea298b2a | ||
|
|
fb4a8ccc0e | ||
|
|
14ee1dc45d | ||
|
|
1320b12699 | ||
|
|
aa20e70671 | ||
|
|
a4ca1f32d3 | ||
|
|
58eb670fed | ||
|
|
7b1b81c45a | ||
|
|
ea9bf07126 | ||
|
|
11789e78fe | ||
|
|
96c75a32c8 |
@@ -0,0 +1,127 @@
|
||||
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: Generate stream-ticket keys
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$DEPLOY_PATH"
|
||||
$COMPOSE run --rm --no-deps keys-init
|
||||
|
||||
# The migrations are baked into the migrator image, so it is rebuilt on
|
||||
# every deploy (plain `up` would reuse the stale image and re-apply the
|
||||
# old migrations). `run --rm` starts wait-for-postgres, blocks until the
|
||||
# migration finishes and propagates its exit code, so a failed migration
|
||||
# aborts the deploy before the new API is started against an old schema.
|
||||
- name: Migrate database
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd "$DEPLOY_PATH"
|
||||
$COMPOSE build migrate
|
||||
$COMPOSE run --rm 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
|
||||
+3
-1
@@ -7,6 +7,9 @@ obj/
|
||||
*.userosscache
|
||||
.vs/
|
||||
artifacts/
|
||||
TestResults/
|
||||
*.trx
|
||||
*.coverage
|
||||
|
||||
# Rider / VS
|
||||
.idea/
|
||||
@@ -25,5 +28,4 @@ Thumbs.db
|
||||
# Deploy secrets and runtime state
|
||||
deploy/keys/
|
||||
deploy/secrets/
|
||||
deploy/nginx/certs/
|
||||
deploy/zitadel/.output/
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 })
|
||||
.Select(albumTrack => new { albumTrack.Track, ListensCount = albumTrack.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.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);
|
||||
.Where(history => history.UserId == request.UserId && history.IsCounted);
|
||||
|
||||
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,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);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using Mediator;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Genres
|
||||
{
|
||||
public record GetAvailableGenresQuery : IQuery<IReadOnlyList<AvailableGenreResponse>>;
|
||||
|
||||
public record AvailableGenreResponse(Genre Genre, IReadOnlyList<Genre> IncompatibleWith);
|
||||
|
||||
public class GetAvailableGenresQueryHandler
|
||||
: IQueryHandler<GetAvailableGenresQuery, IReadOnlyList<AvailableGenreResponse>>
|
||||
{
|
||||
public ValueTask<IReadOnlyList<AvailableGenreResponse>> Handle(GetAvailableGenresQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var all = Enum.GetValues<Genre>();
|
||||
|
||||
IReadOnlyList<AvailableGenreResponse> result = all
|
||||
.Select(genre => new AvailableGenreResponse(
|
||||
genre,
|
||||
all.Where(other => !GenreCompatibility.AreCompatible(genre, other)).ToList()))
|
||||
.ToList();
|
||||
|
||||
return ValueTask.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Genres
|
||||
{
|
||||
public record GetGenresQuery : IQuery<IReadOnlyList<GenreResponse>>;
|
||||
|
||||
public record GenreResponse(Genre Genre, int TrackCount);
|
||||
|
||||
public class GetGenresQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetGenresQuery, IReadOnlyList<GenreResponse>>
|
||||
{
|
||||
public async ValueTask<IReadOnlyList<GenreResponse>> Handle(GetGenresQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var rows = await database.TrackTags
|
||||
.AsNoTracking()
|
||||
.Where(tag => database.UserHasTracks.Any(userTrack => userTrack.TrackId == tag.TrackId))
|
||||
.GroupBy(tag => tag.Tag)
|
||||
.Select(group => new { Genre = group.Key, TrackCount = group.Select(tag => tag.TrackId).Distinct().Count() })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return rows
|
||||
.OrderByDescending(row => row.TrackCount)
|
||||
.ThenBy(row => row.Genre)
|
||||
.Select(row => new GenreResponse(row.Genre, row.TrackCount))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 })
|
||||
.Select(like => new { like.Track, ListensCount = like.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -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,
|
||||
@@ -43,7 +38,7 @@ namespace Musify.Application.Mixes
|
||||
|
||||
var historyTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(history => history.UserId == userId)
|
||||
.Where(history => history.UserId == userId && history.IsCounted)
|
||||
.Select(history => history.TrackId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -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(
|
||||
@@ -21,7 +19,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 })
|
||||
.Select(track => new { Track = track, ListensCount = track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.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 })
|
||||
.Select(plt => new { plt.Track, ListensCount = plt.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -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,7 @@ 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.
|
||||
Guid? listenId = null;
|
||||
if (userId != null)
|
||||
{
|
||||
var newListeningHistory = new ListeningHistory
|
||||
@@ -41,9 +40,10 @@ 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);
|
||||
return new TrackStreamResponse(manifestUrl, ticket.Token, ticket.ExpiresInSeconds, listenId);
|
||||
}
|
||||
|
||||
private long? AnonymousFragmentBytes(long? userId)
|
||||
|
||||
@@ -18,7 +18,8 @@ namespace Musify.Application.Tracks
|
||||
string Title,
|
||||
Guid PictureIntentId,
|
||||
Guid AudioIntentId,
|
||||
IReadOnlyCollection<Genre> Tags)
|
||||
IReadOnlyCollection<Genre> Tags,
|
||||
bool IsExplicit = false)
|
||||
: ICommand<ErrorOr<TrackApplicationResponse>>;
|
||||
|
||||
public class CreateTrackCommandHandler(
|
||||
@@ -83,6 +84,7 @@ namespace Musify.Application.Tracks
|
||||
NormalizedTitle = request.Title.ToUpperInvariant(),
|
||||
OwnerUserId = user.Id,
|
||||
Owner = user,
|
||||
IsExplicit = request.IsExplicit,
|
||||
Pictures = new TrackPictures
|
||||
{
|
||||
OriginalName = pictureIntent.ObjectName,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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))
|
||||
.Select(t => TrackApplicationResponse.FromEntity(t.Track, t.Track.ListeningHistories.Count(l => l.IsCounted)))
|
||||
.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)
|
||||
.Where(l => l.UserId == query.UserId && l.IsCounted)
|
||||
.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 })
|
||||
.Select(t => new { t.Id, Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.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 })
|
||||
.Select(t => new { Track = t, ListensCount = t.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.SingleOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (entity == null)
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
|
||||
@@ -5,10 +5,11 @@ using Musify.Application.Tracks.Responses;
|
||||
using X.PagedList;
|
||||
using X.PagedList.EF;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Domain.ValueObjects;
|
||||
|
||||
namespace Musify.Application.Tracks
|
||||
{
|
||||
public record GetTracksQuery(string? Name, int PageNumber, int PageSize)
|
||||
public record GetTracksQuery(string? Name, int PageNumber, int PageSize, Genre? Genre = null)
|
||||
: IQuery<ErrorOr<TracksSearchResponse>>;
|
||||
|
||||
public class GetTracksQueryHandler(IDatabase database)
|
||||
@@ -28,12 +29,15 @@ namespace Musify.Application.Tracks
|
||||
tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName));
|
||||
}
|
||||
|
||||
if (request.Genre is { } genre)
|
||||
tracksQuery = tracksQuery.Where(t => t.Track.Tags.Any(tag => tag.Tag == genre));
|
||||
|
||||
var totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||
|
||||
var pagedEntities = await tracksQuery
|
||||
.OrderByDescending(t => t.Track.CreatedAt)
|
||||
.ThenBy(t => t.Id)
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.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 })
|
||||
.Select(t => new { t.Track, ListensCount = t.Track.ListeningHistories.Count(l => l.IsCounted) })
|
||||
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||
|
||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,8 @@ namespace Musify.Application.Tracks.Responses
|
||||
DateTime CreatedAt,
|
||||
DateTime UpdatedAt,
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
|
||||
IReadOnlyCollection<Genre> Tags)
|
||||
IReadOnlyCollection<Genre> Tags,
|
||||
bool IsExplicit)
|
||||
{
|
||||
public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
|
||||
new(
|
||||
@@ -28,6 +29,7 @@ namespace Musify.Application.Tracks.Responses
|
||||
track.CreatedAt,
|
||||
track.UpdatedAt,
|
||||
track.OwnerUserId,
|
||||
track.Tags.Select(tag => tag.Tag).ToList());
|
||||
track.Tags.Select(tag => tag.Tag).ToList(),
|
||||
track.IsExplicit);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@ namespace Musify.Application.Tracks.Responses
|
||||
public record TrackStreamResponse(
|
||||
string ManifestUrl,
|
||||
string Ticket,
|
||||
int ExpiresInSeconds);
|
||||
int ExpiresInSeconds,
|
||||
Guid? ListenId = null);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public static class FollowVisibility
|
||||
{
|
||||
public static async Task<bool> CanViewFollowersAsync(
|
||||
IDatabase database,
|
||||
long? viewerId,
|
||||
long userId,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (viewerId is not { } viewer)
|
||||
return false;
|
||||
|
||||
if (viewer == userId)
|
||||
return true;
|
||||
|
||||
var mutualRows = await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.CountAsync(f =>
|
||||
(f.FollowerId == viewer && f.FollowedId == userId) ||
|
||||
(f.FollowerId == userId && f.FollowedId == viewer),
|
||||
cancellationToken);
|
||||
|
||||
return mutualRows == 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,17 +20,18 @@ 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) => t.DurationSeconds)
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
|
||||
.SumAsync(cancellationToken);
|
||||
|
||||
var listenedDates = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId)
|
||||
.Where(l => l.UserId == request.UserId && l.IsCounted)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public record GetUserFollowersQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return Error.NotFound();
|
||||
|
||||
if (!await FollowVisibility.CanViewFollowersAsync(database, request.ViewerId, request.UserId, cancellationToken))
|
||||
return Error.Forbidden();
|
||||
|
||||
var followsQuery = database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowedId == request.UserId);
|
||||
|
||||
var totalCount = await followsQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
var page = await followsQuery
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Follower.Id,
|
||||
f.Follower.Name,
|
||||
f.Follower.FirstName,
|
||||
f.Follower.SecondName,
|
||||
f.Follower.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowerId)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Shared;
|
||||
using Musify.Application.Users.Responses;
|
||||
using X.PagedList.EF;
|
||||
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public record GetUserFollowingQuery(long? ViewerId, long UserId, int PageNumber, int PageSize)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUserFollowingQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserFollowingQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUserFollowingQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!await database.Users.AnyAsync(u => u.Id == request.UserId, cancellationToken))
|
||||
return Error.NotFound();
|
||||
|
||||
var followsQuery = database.UserFollows
|
||||
.AsNoTracking()
|
||||
.Where(f => f.FollowerId == request.UserId);
|
||||
|
||||
var totalCount = await followsQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
|
||||
var page = await followsQuery
|
||||
.OrderByDescending(f => f.CreatedAt)
|
||||
.Select(f => new UserSummaryResponse(
|
||||
f.Followed.Id,
|
||||
f.Followed.Name,
|
||||
f.Followed.FirstName,
|
||||
f.Followed.SecondName,
|
||||
f.Followed.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(v => v.FollowerId == viewerId && v.FollowedId == f.FollowedId)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ using Musify.Application.Serialization;
|
||||
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public record GetUserProfileQuery(long UserId) : IQuery<ErrorOr<UserProfileResponse>>;
|
||||
public record GetUserProfileQuery(long UserId, long? ViewerId = null) : IQuery<ErrorOr<UserProfileResponse>>;
|
||||
|
||||
public record UserProfileResponse(
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
||||
@@ -17,7 +17,9 @@ namespace Musify.Application.Users
|
||||
string? ProfilePictureUrl,
|
||||
DateTime CreatedAt,
|
||||
int FollowersCount,
|
||||
int FollowingCount);
|
||||
int FollowingCount,
|
||||
bool IsFollowing,
|
||||
bool CanViewFollowers);
|
||||
|
||||
public class GetUserProfileQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
||||
@@ -49,6 +51,14 @@ namespace Musify.Application.Users
|
||||
.AsNoTracking()
|
||||
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
|
||||
|
||||
var isFollowing = request.ViewerId is { } viewerId && viewerId != request.UserId &&
|
||||
await database.UserFollows
|
||||
.AsNoTracking()
|
||||
.AnyAsync(f => f.FollowerId == viewerId && f.FollowedId == request.UserId, cancellationToken);
|
||||
|
||||
var canViewFollowers = await FollowVisibility.CanViewFollowersAsync(
|
||||
database, request.ViewerId, request.UserId, cancellationToken);
|
||||
|
||||
return new UserProfileResponse(
|
||||
user.Id,
|
||||
user.Name,
|
||||
@@ -57,7 +67,9 @@ namespace Musify.Application.Users
|
||||
user.ProfilePictureUrl,
|
||||
user.CreatedAt,
|
||||
followersCount,
|
||||
followingCount);
|
||||
followingCount,
|
||||
isFollowing,
|
||||
canViewFollowers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ using Musify.Application.Contracts;
|
||||
|
||||
namespace Musify.Application.Users
|
||||
{
|
||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserApplicationResponse>>>;
|
||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
|
||||
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||
|
||||
public class GetUsersQueryHandler(IDatabase database)
|
||||
: IQueryHandler<GetUsersQuery, ErrorOr<PaginatedResponse<UserApplicationResponse>>>
|
||||
: IQueryHandler<GetUsersQuery, ErrorOr<PaginatedResponse<UserSummaryResponse>>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserApplicationResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserSummaryResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking()
|
||||
@@ -27,11 +27,19 @@ namespace Musify.Application.Users
|
||||
}
|
||||
|
||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||
var viewerId = request.ViewerId;
|
||||
var pagedUsers = await usersQuery
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.OrderBy(u => u.Name)
|
||||
.Select(u => new UserSummaryResponse(
|
||||
u.Id,
|
||||
u.Name,
|
||||
u.FirstName,
|
||||
u.SecondName,
|
||||
u.ProfilePictureUrl,
|
||||
viewerId != null && database.UserFollows.Any(f => f.FollowerId == viewerId && f.FollowedId == u.Id)))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUsers);
|
||||
return PaginatedResponse<UserSummaryResponse>.FromPagedList(pagedUsers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Text.Json.Serialization;
|
||||
using Musify.Application.Serialization;
|
||||
|
||||
namespace Musify.Application.Users.Responses
|
||||
{
|
||||
public record UserSummaryResponse(
|
||||
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
||||
string Name,
|
||||
string? FirstName,
|
||||
string? SecondName,
|
||||
string? ProfilePictureUrl,
|
||||
bool IsFollowedByViewer);
|
||||
}
|
||||
@@ -19,6 +19,12 @@ 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; }
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ namespace Musify.Domain.Entities
|
||||
[Required]
|
||||
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
||||
|
||||
[Required]
|
||||
public bool IsExplicit { get; set; }
|
||||
|
||||
[Required]
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Jobs
|
||||
{
|
||||
var userIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(history => history.IsCounted)
|
||||
.Select(history => history.UserId)
|
||||
.Distinct()
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
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,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;
|
||||
@@ -42,7 +37,7 @@ namespace Musify.Infrastructure.Persistence
|
||||
}
|
||||
else if (entry.State == EntityState.Modified)
|
||||
{
|
||||
entry.Entity.UpdatedAt = now;
|
||||
entry.Property(nameof(IAuditable.UpdatedAt)).CurrentValue = now;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,9 +13,13 @@ 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"));
|
||||
optionsBuilder
|
||||
.UseNpgsql(configuration.ConnectionString, o => o.MapEnum<Genre>("genre"))
|
||||
.AddInterceptors(AuditInterceptor);
|
||||
}
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
|
||||
@@ -18,10 +18,7 @@ namespace Musify.Infrastructure.Persistence
|
||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
||||
|
||||
serviceDescriptors.AddSingleton<AuditableEntityInterceptor>();
|
||||
|
||||
serviceDescriptors.AddDbContext<Database>((serviceProvider, options) =>
|
||||
options.AddInterceptors(serviceProvider.GetRequiredService<AuditableEntityInterceptor>()));
|
||||
serviceDescriptors.AddDbContext<Database>();
|
||||
|
||||
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
|
||||
|
||||
+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(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user