Compare commits
115
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35d6314ce8 | ||
|
|
f99f74f4a1 | ||
|
|
f2765e2e7b | ||
|
|
7b8f3f1222 | ||
|
|
c33b0e8bfd | ||
|
|
87c3f97529 | ||
|
|
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
|
*.userosscache
|
||||||
.vs/
|
.vs/
|
||||||
artifacts/
|
artifacts/
|
||||||
|
TestResults/
|
||||||
|
*.trx
|
||||||
|
*.coverage
|
||||||
|
|
||||||
# Rider / VS
|
# Rider / VS
|
||||||
.idea/
|
.idea/
|
||||||
@@ -25,5 +28,4 @@ Thumbs.db
|
|||||||
# Deploy secrets and runtime state
|
# Deploy secrets and runtime state
|
||||||
deploy/keys/
|
deploy/keys/
|
||||||
deploy/secrets/
|
deploy/secrets/
|
||||||
deploy/nginx/certs/
|
|
||||||
deploy/zitadel/.output/
|
deploy/zitadel/.output/
|
||||||
|
|||||||
@@ -1,57 +1,55 @@
|
|||||||
# Musify
|
# Musify
|
||||||
|
|
||||||
Music streaming platform: upload your own tracks, process them asynchronously
|
A music streaming app. Upload your own tracks, organize them into playlists
|
||||||
(audio transcoded to `.m4a`, thumbnails generated) and stream them without the
|
and albums, and stream them back with a web player built around your
|
||||||
bytes ever going through the API.
|
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
|
backend/ .NET solution: Api, Worker, StreamingGateway, and the shared Domain/Application/Infrastructure
|
||||||
web-player/ SvelteKit 2 + Svelte 5 + Tailwind 4 web client
|
web-player/ the SvelteKit web client
|
||||||
deploy/ Docker Compose stacks — self-contained, no wrapper scripts
|
deploy/ Docker Compose stacks for dev and production
|
||||||
docs/ What each piece does and why (in Spanish)
|
docs/ architecture notes, one file per area
|
||||||
```
|
```
|
||||||
|
|
||||||
```sh
|
Postgres, Zitadel, SeaweedFS, RabbitMQ and Jaeger are not part of this
|
||||||
dotnet test backend/Musify.slnx # backend/Tests/ — Docker running (Testcontainers: Postgres, SeaweedFS)
|
layout. They live in the separate `Infrastructure` repository instead.
|
||||||
```
|
|
||||||
|
|
||||||
## Run it
|
## Digging deeper
|
||||||
|
|
||||||
Development (Docker Desktop):
|
[docs/](docs/README.md) covers how each piece works and why: architecture,
|
||||||
|
authentication, storage, streaming, media processing, and the local dev
|
||||||
```sh
|
setup. [deploy/README.md](deploy/README.md) has the full instructions for
|
||||||
docker compose -f deploy/compose.yml -f deploy/compose.dev.yml up -d
|
running the stack, in development or on a production server.
|
||||||
```
|
|
||||||
|
|
||||||
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
|
var pagedEntities = await tracksQuery
|
||||||
.OrderBy(albumTrack => albumTrack.TrackNumber)
|
.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);
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ namespace Musify.Application.Albums
|
|||||||
{
|
{
|
||||||
var listenedTrackIds = database.ListeningHistories
|
var listenedTrackIds = database.ListeningHistories
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(history => history.UserId == request.UserId);
|
.Where(history => history.UserId == request.UserId && history.IsCounted);
|
||||||
|
|
||||||
var ranked = await database.AlbumHasTracks
|
var ranked = await database.AlbumHasTracks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Musify.Application.Albums.Responses
|
namespace Musify.Application.Albums.Responses
|
||||||
{
|
{
|
||||||
/// <summary>A page of the user's catalog albums (their own).</summary>
|
|
||||||
public record AlbumsSearchResponse(
|
public record AlbumsSearchResponse(
|
||||||
IReadOnlyList<AlbumSearchItemResponse> Items,
|
IReadOnlyList<AlbumSearchItemResponse> Items,
|
||||||
int PageNumber,
|
int PageNumber,
|
||||||
|
|||||||
@@ -77,8 +77,6 @@ namespace Musify.Application.Albums
|
|||||||
finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
database.Albums.Update(album);
|
|
||||||
|
|
||||||
if (pictureIntent != null && finalPictureKey != null)
|
if (pictureIntent != null && finalPictureKey != null)
|
||||||
{
|
{
|
||||||
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
|
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ using Musify.Application.Configuration.Responses;
|
|||||||
|
|
||||||
namespace Musify.Application.Configuration
|
namespace Musify.Application.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
|
|
||||||
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
|
|
||||||
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
|
||||||
|
|
||||||
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
|
||||||
|
|||||||
@@ -2,28 +2,16 @@ using System.ComponentModel.DataAnnotations;
|
|||||||
|
|
||||||
namespace Musify.Application.Configuration
|
namespace Musify.Application.Configuration
|
||||||
{
|
{
|
||||||
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
|
|
||||||
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
|
|
||||||
public sealed class PlaybackConfiguration
|
public sealed class PlaybackConfiguration
|
||||||
{
|
{
|
||||||
public const string SectionName = "Playback";
|
public const string SectionName = "Playback";
|
||||||
|
|
||||||
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
|
|
||||||
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
|
|
||||||
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
|
|
||||||
public bool AllowAnonymousListening { get; set; }
|
public bool AllowAnonymousListening { get; set; }
|
||||||
|
|
||||||
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
|
|
||||||
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
|
|
||||||
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
|
|
||||||
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
|
|
||||||
[Range(0, 3600)]
|
[Range(0, 3600)]
|
||||||
public int AnonymousFragmentSeconds { get; set; }
|
public int AnonymousFragmentSeconds { get; set; }
|
||||||
|
|
||||||
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
|
|
||||||
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
|
|
||||||
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
|
|
||||||
/// ever changes, since the two aren't wired together.</summary>
|
|
||||||
[Range(1, int.MaxValue)]
|
[Range(1, int.MaxValue)]
|
||||||
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-3
@@ -1,7 +1,5 @@
|
|||||||
namespace Musify.Application.Configuration.Responses
|
namespace Musify.Application.Configuration.Responses
|
||||||
{
|
{
|
||||||
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
|
|
||||||
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
|
|
||||||
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
|
|
||||||
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,7 @@ namespace Musify.Application.Contracts
|
|||||||
{
|
{
|
||||||
public interface IStreamTicketService
|
public interface IStreamTicketService
|
||||||
{
|
{
|
||||||
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
|
|
||||||
/// <c>sub</c> claim.</param>
|
|
||||||
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
|
|
||||||
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
|
|
||||||
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
|
|
||||||
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
var pagedEntities = await likesQuery
|
||||||
.OrderByDescending(like => like.CreatedAt)
|
.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);
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ namespace Musify.Application.Mixes
|
|||||||
public record GenerateMixesForUserCommand(long UserId)
|
public record GenerateMixesForUserCommand(long UserId)
|
||||||
: ICommand<ErrorOr<Success>>;
|
: ICommand<ErrorOr<Success>>;
|
||||||
|
|
||||||
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
|
|
||||||
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
|
|
||||||
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
|
|
||||||
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
|
|
||||||
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
|
|
||||||
public class GenerateMixesForUserCommandHandler(
|
public class GenerateMixesForUserCommandHandler(
|
||||||
IDatabase database,
|
IDatabase database,
|
||||||
MixConfiguration mixConfiguration,
|
MixConfiguration mixConfiguration,
|
||||||
@@ -43,7 +38,7 @@ namespace Musify.Application.Mixes
|
|||||||
|
|
||||||
var historyTrackIds = await database.ListeningHistories
|
var historyTrackIds = await database.ListeningHistories
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(history => history.UserId == userId)
|
.Where(history => history.UserId == userId && history.IsCounted)
|
||||||
.Select(history => history.TrackId)
|
.Select(history => history.TrackId)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ using Musify.Domain.Entities;
|
|||||||
|
|
||||||
namespace Musify.Application.Mixes
|
namespace Musify.Application.Mixes
|
||||||
{
|
{
|
||||||
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
|
|
||||||
/// track lookup, instead of an EF Include chain deep enough (Mix -> Items -> Track -> Owner) to
|
|
||||||
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
|
|
||||||
internal static class MixItemMapper
|
internal static class MixItemMapper
|
||||||
{
|
{
|
||||||
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
|
||||||
@@ -21,7 +19,7 @@ namespace Musify.Application.Mixes
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(track => track.Owner)
|
.Include(track => track.Owner)
|
||||||
.Where(track => trackIds.Contains(track.Id))
|
.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);
|
.ToDictionaryAsync(x => x.Track.Id, cancellationToken);
|
||||||
|
|
||||||
return orderedItems
|
return orderedItems
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ namespace Musify.Application.PlayLists
|
|||||||
}, validation.Value);
|
}, validation.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
private async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||||
Guid playListId,
|
Guid playListId,
|
||||||
UploadIntent pictureIntent,
|
UploadIntent pictureIntent,
|
||||||
string finalPictureKey,
|
string finalPictureKey,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ namespace Musify.Application.PlayLists
|
|||||||
|
|
||||||
var pagedEntities = await tracksQuery
|
var pagedEntities = await tracksQuery
|
||||||
.OrderBy(plt => plt.Position)
|
.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);
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
||||||
|
|||||||
@@ -85,8 +85,6 @@ namespace Musify.Application.PlayLists
|
|||||||
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
database.PlayLists.Update(playListEntity);
|
|
||||||
|
|
||||||
if (pictureIntent != null && finalPictureKey != null)
|
if (pictureIntent != null && finalPictureKey != null)
|
||||||
{
|
{
|
||||||
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
||||||
@@ -117,7 +115,7 @@ namespace Musify.Application.PlayLists
|
|||||||
return PlayListApplicationResponse.FromEntity(playListEntity, coverTrackIds);
|
return PlayListApplicationResponse.FromEntity(playListEntity, coverTrackIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
private async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||||
Guid playListId,
|
Guid playListId,
|
||||||
UploadIntent pictureIntent,
|
UploadIntent pictureIntent,
|
||||||
string finalPictureKey,
|
string finalPictureKey,
|
||||||
|
|||||||
@@ -4,13 +4,6 @@ using System.Text.Json.Serialization;
|
|||||||
|
|
||||||
namespace Musify.Application.Serialization;
|
namespace Musify.Application.Serialization;
|
||||||
|
|
||||||
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
|
|
||||||
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
|
|
||||||
// them to the nearest representable double, corrupting the id (it becomes a
|
|
||||||
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
|
|
||||||
// on the specific id-shaped `long`/`long?` response properties (not globally —
|
|
||||||
// unrelated `long` fields like byte sizes must stay numbers), this serializes
|
|
||||||
// them as a JSON string instead, which round-trips exactly.
|
|
||||||
public sealed class LongAsStringConverter : JsonConverter<long>
|
public sealed class LongAsStringConverter : JsonConverter<long>
|
||||||
{
|
{
|
||||||
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
|
||||||
|
|||||||
@@ -30,8 +30,7 @@ public sealed class TrackStreamIssuer(
|
|||||||
folderPath,
|
folderPath,
|
||||||
streamGatewayConfiguration.AudioFileName);
|
streamGatewayConfiguration.AudioFileName);
|
||||||
|
|
||||||
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
|
Guid? listenId = null;
|
||||||
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
|
|
||||||
if (userId != null)
|
if (userId != null)
|
||||||
{
|
{
|
||||||
var newListeningHistory = new ListeningHistory
|
var newListeningHistory = new ListeningHistory
|
||||||
@@ -41,9 +40,10 @@ public sealed class TrackStreamIssuer(
|
|||||||
};
|
};
|
||||||
await database.ListeningHistories.AddAsync(newListeningHistory, cancellationToken);
|
await database.ListeningHistories.AddAsync(newListeningHistory, cancellationToken);
|
||||||
await database.SaveChangesAsync(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)
|
private long? AnonymousFragmentBytes(long? userId)
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ namespace Musify.Application.Tracks
|
|||||||
string Title,
|
string Title,
|
||||||
Guid PictureIntentId,
|
Guid PictureIntentId,
|
||||||
Guid AudioIntentId,
|
Guid AudioIntentId,
|
||||||
IReadOnlyCollection<Genre> Tags)
|
IReadOnlyCollection<Genre> Tags,
|
||||||
|
bool IsExplicit = false)
|
||||||
: ICommand<ErrorOr<TrackApplicationResponse>>;
|
: ICommand<ErrorOr<TrackApplicationResponse>>;
|
||||||
|
|
||||||
public class CreateTrackCommandHandler(
|
public class CreateTrackCommandHandler(
|
||||||
@@ -83,6 +84,7 @@ namespace Musify.Application.Tracks
|
|||||||
NormalizedTitle = request.Title.ToUpperInvariant(),
|
NormalizedTitle = request.Title.ToUpperInvariant(),
|
||||||
OwnerUserId = user.Id,
|
OwnerUserId = user.Id,
|
||||||
Owner = user,
|
Owner = user,
|
||||||
|
IsExplicit = request.IsExplicit,
|
||||||
Pictures = new TrackPictures
|
Pictures = new TrackPictures
|
||||||
{
|
{
|
||||||
OriginalName = pictureIntent.ObjectName,
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,11 +14,12 @@ namespace Musify.Application.Tracks
|
|||||||
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var track = await database.ListeningHistories
|
var track = await database.ListeningHistories
|
||||||
|
.AsNoTracking()
|
||||||
.Include(t => t.Track.Owner)
|
.Include(t => t.Track.Owner)
|
||||||
.Include(t => t.Track.Tags)
|
.Include(t => t.Track.Tags)
|
||||||
.Where(t => t.UserId == query.UserId)
|
.Where(t => t.UserId == query.UserId)
|
||||||
.OrderByDescending(t => t.ListenedAt)
|
.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);
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
return track == null
|
return track == null
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ namespace Musify.Application.Tracks
|
|||||||
public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
|
public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var recentTrackIds = await database.ListeningHistories
|
var recentTrackIds = await database.ListeningHistories
|
||||||
.Where(l => l.UserId == query.UserId)
|
.AsNoTracking()
|
||||||
|
.Where(l => l.UserId == query.UserId && l.IsCounted)
|
||||||
.GroupBy(l => l.TrackId)
|
.GroupBy(l => l.TrackId)
|
||||||
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
||||||
.Select(g => g.Key)
|
.Select(g => g.Key)
|
||||||
@@ -24,10 +25,11 @@ namespace Musify.Application.Tracks
|
|||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var tracksById = await database.Tracks
|
var tracksById = await database.Tracks
|
||||||
|
.AsNoTracking()
|
||||||
.Include(t => t.Owner)
|
.Include(t => t.Owner)
|
||||||
.Include(t => t.Tags)
|
.Include(t => t.Tags)
|
||||||
.Where(t => recentTrackIds.Contains(t.Id))
|
.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);
|
.ToDictionaryAsync(t => t.Id, cancellationToken);
|
||||||
|
|
||||||
return recentTrackIds
|
return recentTrackIds
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Musify.Application.Tracks
|
|||||||
.Include(t => t.Owner)
|
.Include(t => t.Owner)
|
||||||
.Include(t => t.Tags)
|
.Include(t => t.Tags)
|
||||||
.Where(t => t.Id == request.TrackId)
|
.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);
|
.SingleOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
if (entity == null)
|
if (entity == null)
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ using Musify.Application.Tracks.Responses;
|
|||||||
|
|
||||||
namespace Musify.Application.Tracks
|
namespace Musify.Application.Tracks
|
||||||
{
|
{
|
||||||
/// <param name="UserId">Null for an anonymous request — allowed only when
|
|
||||||
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
|
|
||||||
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
|
||||||
: IQuery<ErrorOr<TrackStreamResponse>>;
|
: IQuery<ErrorOr<TrackStreamResponse>>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ using Musify.Application.Tracks.Responses;
|
|||||||
using X.PagedList;
|
using X.PagedList;
|
||||||
using X.PagedList.EF;
|
using X.PagedList.EF;
|
||||||
using Musify.Application.Contracts;
|
using Musify.Application.Contracts;
|
||||||
|
using Musify.Domain.ValueObjects;
|
||||||
|
|
||||||
namespace Musify.Application.Tracks
|
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>>;
|
: IQuery<ErrorOr<TracksSearchResponse>>;
|
||||||
|
|
||||||
public class GetTracksQueryHandler(IDatabase database)
|
public class GetTracksQueryHandler(IDatabase database)
|
||||||
@@ -28,12 +29,15 @@ namespace Musify.Application.Tracks
|
|||||||
tracksQuery = tracksQuery.Where(t => t.Track.NormalizedTitle.Contains(normalizedName));
|
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 totalCount = await tracksQuery.CountAsync(cancellationToken);
|
||||||
|
|
||||||
var pagedEntities = await tracksQuery
|
var pagedEntities = await tracksQuery
|
||||||
.OrderByDescending(t => t.Track.CreatedAt)
|
.OrderByDescending(t => t.Track.CreatedAt)
|
||||||
.ThenBy(t => t.Id)
|
.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);
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
var items = pagedEntities
|
var items = pagedEntities
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ namespace Musify.Application.Tracks
|
|||||||
|
|
||||||
var pagedEntities = await tracksQuery
|
var pagedEntities = await tracksQuery
|
||||||
.OrderBy(t => t.Track.CreatedAt)
|
.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);
|
.ToPagedListAsync(pageNumber, pageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
var pagedTracks = new StaticPagedList<TrackApplicationResponse>(
|
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 CreatedAt,
|
||||||
DateTime UpdatedAt,
|
DateTime UpdatedAt,
|
||||||
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
|
[property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId,
|
||||||
IReadOnlyCollection<Genre> Tags)
|
IReadOnlyCollection<Genre> Tags,
|
||||||
|
bool IsExplicit)
|
||||||
{
|
{
|
||||||
public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
|
public static TrackApplicationResponse FromEntity(Track track, int listensCount) =>
|
||||||
new(
|
new(
|
||||||
@@ -28,6 +29,7 @@ namespace Musify.Application.Tracks.Responses
|
|||||||
track.CreatedAt,
|
track.CreatedAt,
|
||||||
track.UpdatedAt,
|
track.UpdatedAt,
|
||||||
track.OwnerUserId,
|
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(
|
public record TrackStreamResponse(
|
||||||
string ManifestUrl,
|
string ManifestUrl,
|
||||||
string Ticket,
|
string Ticket,
|
||||||
int ExpiresInSeconds);
|
int ExpiresInSeconds,
|
||||||
|
Guid? ListenId = null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace Musify.Application.Tracks.Responses
|
namespace Musify.Application.Tracks.Responses
|
||||||
{
|
{
|
||||||
/// <summary>A page of the user's catalog tracks (their own uploads).</summary>
|
|
||||||
public record TracksSearchResponse(
|
public record TracksSearchResponse(
|
||||||
IReadOnlyList<TrackSearchItemResponse> Items,
|
IReadOnlyList<TrackSearchItemResponse> Items,
|
||||||
int PageNumber,
|
int PageNumber,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,19 +16,22 @@ namespace Musify.Application.Users
|
|||||||
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
||||||
|
|
||||||
var thisWeekHistory = database.ListeningHistories
|
var thisWeekHistory = database.ListeningHistories
|
||||||
|
.AsNoTracking()
|
||||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||||
|
|
||||||
var tracksThisWeek = await thisWeekHistory
|
var tracksThisWeek = await thisWeekHistory
|
||||||
|
.Where(l => l.IsCounted)
|
||||||
.Select(l => l.TrackId)
|
.Select(l => l.TrackId)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.CountAsync(cancellationToken);
|
.CountAsync(cancellationToken);
|
||||||
|
|
||||||
var secondsThisWeek = await thisWeekHistory
|
var secondsThisWeek = await thisWeekHistory
|
||||||
.Join(database.Tracks, l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
|
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => l.PlayedSeconds ?? (l.IsCounted ? t.DurationSeconds : 0))
|
||||||
.SumAsync(cancellationToken);
|
.SumAsync(cancellationToken);
|
||||||
|
|
||||||
var listenedDates = await database.ListeningHistories
|
var listenedDates = await database.ListeningHistories
|
||||||
.Where(l => l.UserId == request.UserId)
|
.AsNoTracking()
|
||||||
|
.Where(l => l.UserId == request.UserId && l.IsCounted)
|
||||||
.Select(l => l.ListenedAt.Date)
|
.Select(l => l.ListenedAt.Date)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync(cancellationToken);
|
.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
|
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(
|
public record UserProfileResponse(
|
||||||
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
[property: JsonConverter(typeof(LongAsStringConverter))] long Id,
|
||||||
@@ -17,7 +17,9 @@ namespace Musify.Application.Users
|
|||||||
string? ProfilePictureUrl,
|
string? ProfilePictureUrl,
|
||||||
DateTime CreatedAt,
|
DateTime CreatedAt,
|
||||||
int FollowersCount,
|
int FollowersCount,
|
||||||
int FollowingCount);
|
int FollowingCount,
|
||||||
|
bool IsFollowing,
|
||||||
|
bool CanViewFollowers);
|
||||||
|
|
||||||
public class GetUserProfileQueryHandler(IDatabase database)
|
public class GetUserProfileQueryHandler(IDatabase database)
|
||||||
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
: IQueryHandler<GetUserProfileQuery, ErrorOr<UserProfileResponse>>
|
||||||
@@ -49,6 +51,14 @@ namespace Musify.Application.Users
|
|||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.CountAsync(f => f.FollowerId == request.UserId, cancellationToken);
|
.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(
|
return new UserProfileResponse(
|
||||||
user.Id,
|
user.Id,
|
||||||
user.Name,
|
user.Name,
|
||||||
@@ -57,7 +67,9 @@ namespace Musify.Application.Users
|
|||||||
user.ProfilePictureUrl,
|
user.ProfilePictureUrl,
|
||||||
user.CreatedAt,
|
user.CreatedAt,
|
||||||
followersCount,
|
followersCount,
|
||||||
followingCount);
|
followingCount,
|
||||||
|
isFollowing,
|
||||||
|
canViewFollowers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,30 +8,38 @@ using Musify.Application.Contracts;
|
|||||||
|
|
||||||
namespace Musify.Application.Users
|
namespace Musify.Application.Users
|
||||||
{
|
{
|
||||||
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch)
|
public record GetUsersQuery(int PageNumber, int PageSize, string? UsernameSearch, long? ViewerId = null)
|
||||||
: IQuery<ErrorOr<PaginatedResponse<UserApplicationResponse> >>;
|
: IQuery<ErrorOr<PaginatedResponse<UserSummaryResponse>>>;
|
||||||
|
|
||||||
public class GetUsersQueryHandler(IDatabase database)
|
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 user = database.Users
|
var usersQuery = database.Users
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.AsQueryable();
|
.AsQueryable();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(request.UsernameSearch))
|
if (!string.IsNullOrEmpty(request.UsernameSearch))
|
||||||
{
|
{
|
||||||
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
|
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
|
||||||
user = user.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
usersQuery = usersQuery.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||||
}
|
}
|
||||||
|
|
||||||
var totalCount = await user.CountAsync(cancellationToken);
|
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||||
var pagedUser = await user
|
var viewerId = request.ViewerId;
|
||||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
var pagedUsers = await usersQuery
|
||||||
|
.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);
|
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||||
|
|
||||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUser);
|
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]
|
[Required]
|
||||||
public DateTime ListenedAt { get; set; } = DateTime.UtcNow;
|
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) )]
|
[ForeignKey(nameof(UserId) )]
|
||||||
public User User { get; set; }
|
public User User { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ namespace Musify.Domain.Entities
|
|||||||
[Required]
|
[Required]
|
||||||
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active;
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
public bool IsExplicit { get; set; }
|
||||||
|
|
||||||
[Required]
|
[Required]
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
namespace Musify.Domain.ValueObjects
|
namespace Musify.Domain.ValueObjects
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
|
|
||||||
/// be tagged as both Classical and Metal at the same time).
|
|
||||||
/// </summary>
|
|
||||||
public static class GenreCompatibility
|
public static class GenreCompatibility
|
||||||
{
|
{
|
||||||
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
|
||||||
@@ -31,9 +28,6 @@ namespace Musify.Domain.ValueObjects
|
|||||||
public static bool AreCompatible(Genre a, Genre b) =>
|
public static bool AreCompatible(Genre a, Genre b) =>
|
||||||
a == b || !IncompatiblePairs.Contains((a, b));
|
a == b || !IncompatiblePairs.Contains((a, b));
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Returns every mutually exclusive genre pair found within the given set of tags.
|
|
||||||
/// </summary>
|
|
||||||
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
|
||||||
{
|
{
|
||||||
var distinctTags = tags.Distinct().ToList();
|
var distinctTags = tags.Distinct().ToList();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ namespace Musify.Infrastructure.Jobs
|
|||||||
{
|
{
|
||||||
var userIds = await database.ListeningHistories
|
var userIds = await database.ListeningHistories
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
|
.Where(history => history.IsCounted)
|
||||||
.Select(history => history.UserId)
|
.Select(history => history.UserId)
|
||||||
.Distinct()
|
.Distinct()
|
||||||
.ToListAsync(cancellationToken);
|
.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)
|
if (context == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// SavingChanges(Async) fires before EF's own automatic change detection for this
|
|
||||||
// save, so entities modified through plain property assignment (query, then set a
|
|
||||||
// property, then SaveChanges — the normal case) still show as Unchanged here unless
|
|
||||||
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
|
|
||||||
// never bumped on a real update, only on inserts.
|
|
||||||
context.ChangeTracker.DetectChanges();
|
context.ChangeTracker.DetectChanges();
|
||||||
|
|
||||||
var now = DateTime.UtcNow;
|
var now = DateTime.UtcNow;
|
||||||
@@ -42,7 +37,7 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
}
|
}
|
||||||
else if (entry.State == EntityState.Modified)
|
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)
|
public class Database(DatabaseConfiguration configuration)
|
||||||
: DbContext, AppIDatabase
|
: DbContext, AppIDatabase
|
||||||
{
|
{
|
||||||
|
private static readonly AuditableEntityInterceptor AuditInterceptor = new();
|
||||||
|
|
||||||
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
{
|
{
|
||||||
optionsBuilder.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)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ namespace Musify.Infrastructure.Persistence
|
|||||||
serviceDescriptors.AddSingleton(serviceProvider =>
|
serviceDescriptors.AddSingleton(serviceProvider =>
|
||||||
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
serviceProvider.GetRequiredService<IOptions<DatabaseConfiguration>>().Value);
|
||||||
|
|
||||||
serviceDescriptors.AddSingleton<AuditableEntityInterceptor>();
|
serviceDescriptors.AddDbContext<Database>();
|
||||||
|
|
||||||
serviceDescriptors.AddDbContext<Database>((serviceProvider, options) =>
|
|
||||||
options.AddInterceptors(serviceProvider.GetRequiredService<AuditableEntityInterceptor>()));
|
|
||||||
|
|
||||||
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
serviceDescriptors.AddScoped<IDatabase>(serviceProvider => serviceProvider.GetRequiredService<Database>());
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
[Migration("20260531234306_AddUserProfilePictureUrl")]
|
||||||
partial class AddUserProfilePictureUrl
|
partial class AddUserProfilePictureUrl
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddUserProfilePictureUrl : Migration
|
public partial class AddUserProfilePictureUrl : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
nullable: true);
|
nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
[Migration("20260601195757_RenameTimestampsAndAudit")]
|
||||||
partial class RenameTimestampsAndAudit
|
partial class RenameTimestampsAndAudit
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class RenameTimestampsAndAudit : Migration
|
public partial class RenameTimestampsAndAudit : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
newName: "CreatedAt");
|
newName: "CreatedAt");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
[Migration("20260705123932_AddTrackProcessingStateSaga")]
|
||||||
partial class AddTrackProcessingStateSaga
|
partial class AddTrackProcessingStateSaga
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddTrackProcessingStateSaga : Migration
|
public partial class AddTrackProcessingStateSaga : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
|
||||||
partial class AddTrackProcessingSagaKeys
|
partial class AddTrackProcessingSagaKeys
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddTrackProcessingSagaKeys : Migration
|
public partial class AddTrackProcessingSagaKeys : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -29,7 +29,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
nullable: true);
|
nullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
|
||||||
partial class AddPlayListProcessingStateSaga
|
partial class AddPlayListProcessingStateSaga
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddPlayListProcessingStateSaga : Migration
|
public partial class AddPlayListProcessingStateSaga : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260713234847_AddListeningHistory")]
|
[Migration("20260713234847_AddListeningHistory")]
|
||||||
partial class AddListeningHistory
|
partial class AddListeningHistory
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddListeningHistory : Migration
|
public partial class AddListeningHistory : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -47,7 +47,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "UserId");
|
column: "UserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260714114537_AddTrackDuration")]
|
[Migration("20260714114537_AddTrackDuration")]
|
||||||
partial class AddTrackDuration
|
partial class AddTrackDuration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddTrackDuration : Migration
|
public partial class AddTrackDuration : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<int>(
|
migrationBuilder.AddColumn<int>(
|
||||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
defaultValue: 0);
|
defaultValue: 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
|
||||||
partial class AddTrackSourceArtistExternalId
|
partial class AddTrackSourceArtistExternalId
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddTrackSourceArtistExternalId : Migration
|
public partial class AddTrackSourceArtistExternalId : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
filter: "\"ExternalId\" <> ''");
|
filter: "\"ExternalId\" <> ''");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260715230836_AddTrackDownloadRequested")]
|
[Migration("20260715230836_AddTrackDownloadRequested")]
|
||||||
partial class AddTrackDownloadRequested
|
partial class AddTrackDownloadRequested
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddTrackDownloadRequested : Migration
|
public partial class AddTrackDownloadRequested : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<bool>(
|
migrationBuilder.AddColumn<bool>(
|
||||||
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
defaultValue: false);
|
defaultValue: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
|
||||||
partial class MakeTrackMediaAndExternalIdNullable
|
partial class MakeTrackMediaAndExternalIdNullable
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
public partial class MakeTrackMediaAndExternalIdNullable : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -66,7 +66,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
|
||||||
partial class AddArtistsAndTrackOwnership
|
partial class AddArtistsAndTrackOwnership
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddArtistsAndTrackOwnership : Migration
|
public partial class AddArtistsAndTrackOwnership : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<long>(
|
migrationBuilder.AddColumn<long>(
|
||||||
@@ -154,7 +154,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260716220047_DropTrackArtistColumn")]
|
[Migration("20260716220047_DropTrackArtistColumn")]
|
||||||
partial class DropTrackArtistColumn
|
partial class DropTrackArtistColumn
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class DropTrackArtistColumn : Migration
|
public partial class DropTrackArtistColumn : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
table: "Tracks");
|
table: "Tracks");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<string>(
|
migrationBuilder.AddColumn<string>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
|
||||||
partial class MovePlayListPicturesToOwnedType
|
partial class MovePlayListPicturesToOwnedType
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,16 +4,15 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class MovePlayListPicturesToOwnedType : Migration
|
public partial class MovePlayListPicturesToOwnedType : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
|
||||||
partial class RenameTrackDurationToDurationSeconds
|
partial class RenameTrackDurationToDurationSeconds
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class RenameTrackDurationToDurationSeconds : Migration
|
public partial class RenameTrackDurationToDurationSeconds : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
@@ -16,7 +16,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
newName: "DurationSeconds");
|
newName: "DurationSeconds");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.RenameColumn(
|
migrationBuilder.RenameColumn(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
|
||||||
partial class SplitTrackIntoLocalAndExternal
|
partial class SplitTrackIntoLocalAndExternal
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class SplitTrackIntoLocalAndExternal : Migration
|
public partial class SplitTrackIntoLocalAndExternal : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
@@ -156,7 +156,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldNullable: true);
|
oldNullable: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropForeignKey(
|
migrationBuilder.DropForeignKey(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
[Migration("20260717223358_MakePlayListPicturesNullable")]
|
||||||
partial class MakePlayListPicturesNullable
|
partial class MakePlayListPicturesNullable
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class MakePlayListPicturesNullable : Migration
|
public partial class MakePlayListPicturesNullable : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -51,7 +51,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldMaxLength: 64);
|
oldMaxLength: 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
backend/Core/Musify.Infrastructure/Persistence/Migrations/20260720180629_WidenTrackTitle.Designer.cs
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720180629_WidenTrackTitle")]
|
[Migration("20260720180629_WidenTrackTitle")]
|
||||||
partial class WidenTrackTitle
|
partial class WidenTrackTitle
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class WidenTrackTitle : Migration
|
public partial class WidenTrackTitle : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
oldMaxLength: 50);
|
oldMaxLength: 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720180747_AddAlbums")]
|
[Migration("20260720180747_AddAlbums")]
|
||||||
partial class AddAlbums
|
partial class AddAlbums
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddAlbums : Migration
|
public partial class AddAlbums : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -100,7 +100,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "OwnerUserId");
|
column: "OwnerUserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260720184656_AddUserAlbumExternalId")]
|
[Migration("20260720184656_AddUserAlbumExternalId")]
|
||||||
partial class AddUserAlbumExternalId
|
partial class AddUserAlbumExternalId
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddUserAlbumExternalId : Migration
|
public partial class AddUserAlbumExternalId : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721155102_AddMixes")]
|
[Migration("20260721155102_AddMixes")]
|
||||||
partial class AddMixes
|
partial class AddMixes
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddMixes : Migration
|
public partial class AddMixes : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -83,7 +83,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "TrackId");
|
column: "TrackId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
[Migration("20260721161653_RemoveUserAlbumExternalId")]
|
||||||
partial class RemoveUserAlbumExternalId
|
partial class RemoveUserAlbumExternalId
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class RemoveUserAlbumExternalId : Migration
|
public partial class RemoveUserAlbumExternalId : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
@@ -24,7 +24,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
column: "OwnerUserId");
|
column: "OwnerUserId");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropIndex(
|
migrationBuilder.DropIndex(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721162102_DropMixGeneratedOn")]
|
[Migration("20260721162102_DropMixGeneratedOn")]
|
||||||
partial class DropMixGeneratedOn
|
partial class DropMixGeneratedOn
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class DropMixGeneratedOn : Migration
|
public partial class DropMixGeneratedOn : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropColumn(
|
migrationBuilder.DropColumn(
|
||||||
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
table: "Mixes");
|
table: "Mixes");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AddColumn<DateOnly>(
|
migrationBuilder.AddColumn<DateOnly>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
[Migration("20260721190735_MakePlayListDescriptionOptional")]
|
||||||
partial class MakePlayListDescriptionOptional
|
partial class MakePlayListDescriptionOptional
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class MakePlayListDescriptionOptional : Migration
|
public partial class MakePlayListDescriptionOptional : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.AlterColumn<string>(
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724170051_DurationAsDouble")]
|
[Migration("20260724170051_DurationAsDouble")]
|
||||||
partial class DurationAsDouble
|
partial class DurationAsDouble
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,16 +4,15 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class DurationAsDouble : Migration
|
public partial class DurationAsDouble : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724181012_AddExternalAlbums")]
|
[Migration("20260724181012_AddExternalAlbums")]
|
||||||
partial class AddExternalAlbums
|
partial class AddExternalAlbums
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
+2
-3
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
namespace Musify.Infrastructure.Persistence.Migrations
|
namespace Musify.Infrastructure.Persistence.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class AddExternalAlbums : Migration
|
public partial class AddExternalAlbums : Migration
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.CreateTable(
|
migrationBuilder.CreateTable(
|
||||||
@@ -36,7 +36,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
unique: true);
|
unique: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
{
|
{
|
||||||
migrationBuilder.DropTable(
|
migrationBuilder.DropTable(
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
|||||||
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
[Migration("20260724182659_AddExternalAlbumThumbnail")]
|
||||||
partial class AddExternalAlbumThumbnail
|
partial class AddExternalAlbumThumbnail
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user