Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
3 changed files with 48 additions and 6 deletions
Showing only changes of commit 1bd6f18c5e - Show all commits
@@ -8,7 +8,7 @@ using Musify.Domain.ValueObjects;
namespace Musify.Application.Services; namespace Musify.Application.Services;
public sealed record YouTubeTrackProvision(Track Track, bool Created, YouTubeSongResult? Song); public sealed record YouTubeTrackProvision(Track Track, YouTubeSongResult? Song);
public sealed class YouTubeTrackProvisioner( public sealed class YouTubeTrackProvisioner(
IDatabase database, IDatabase database,
@@ -26,7 +26,7 @@ public sealed class YouTubeTrackProvisioner(
{ {
var existing = await FindAsync(videoId, cancellationToken); var existing = await FindAsync(videoId, cancellationToken);
if (existing is not null) if (existing is not null)
return new YouTubeTrackProvision(existing, Created: false, Song: null); return new YouTubeTrackProvision(existing, Song: null);
var songResult = await youTubeMusicService.GetSongAsync(videoId, cancellationToken); var songResult = await youTubeMusicService.GetSongAsync(videoId, cancellationToken);
if (songResult.IsError) if (songResult.IsError)
@@ -81,7 +81,7 @@ public sealed class YouTubeTrackProvisioner(
{ {
await database.SaveChangesAsync(cancellationToken); await database.SaveChangesAsync(cancellationToken);
logger.LogInformation("Created YouTube track {VideoId} ({TrackId})", videoId, track.Id); logger.LogInformation("Created YouTube track {VideoId} ({TrackId})", videoId, track.Id);
return new YouTubeTrackProvision(track, Created: true, Song: song); return new YouTubeTrackProvision(track, Song: song);
} }
catch (DbUpdateException) catch (DbUpdateException)
{ {
@@ -92,7 +92,7 @@ public sealed class YouTubeTrackProvisioner(
logger.LogWarning("YouTube track {VideoId} creation raced but could not be refetched", videoId); logger.LogWarning("YouTube track {VideoId} creation raced but could not be refetched", videoId);
return Error.Conflict(description: "The track was just created by another request. Retry the operation."); return Error.Conflict(description: "The track was just created by another request. Retry the operation.");
} }
return new YouTubeTrackProvision(refetched, Created: false, Song: null); return new YouTubeTrackProvision(refetched, Song: null);
} }
} }
@@ -108,6 +108,30 @@ public sealed class YouTubeTrackProvisioner(
if (existing is not null) if (existing is not null)
return existing; return existing;
if (byExternalId)
{
var adoptable = await database.Artists
.SingleOrDefaultAsync(a => a.ExternalId == null && a.UserId == null && a.NormalizedName == normalized, cancellationToken);
if (adoptable is not null)
{
var previousName = adoptable.Name;
adoptable.ExternalId = reference.Id;
adoptable.Name = name;
try
{
await database.SaveChangesAsync(cancellationToken);
return adoptable;
}
catch (DbUpdateException)
{
adoptable.ExternalId = null;
adoptable.Name = previousName;
return await database.Artists.SingleAsync(a => a.ExternalId == reference.Id, cancellationToken);
}
}
}
var artist = new Artist var artist = new Artist
{ {
Name = name, Name = name,
@@ -47,6 +47,14 @@ namespace Musify.Application.Users
user.SecondName = request.SecondName; user.SecondName = request.SecondName;
user.ProfilePictureUrl = request.ProfilePictureUrl; user.ProfilePictureUrl = request.ProfilePictureUrl;
var artist = await database.Artists
.SingleOrDefaultAsync(a => a.UserId == request.Id, cancellationToken);
if (artist is not null)
{
artist.Name = request.Name;
artist.NormalizedName = request.Name.ToUpperInvariant();
}
var written = await database.SaveChangesAsync(cancellationToken); var written = await database.SaveChangesAsync(cancellationToken);
if (written > 0) if (written > 0)
logger.LogInformation("Synced profile for user {UserId}", request.Id); logger.LogInformation("Synced profile for user {UserId}", request.Id);
@@ -108,6 +108,11 @@ namespace Musify.Infrastructure.Persistence.Migrations
WHERE t."Source" = 0 AND ut."TrackId" = t."Id"; WHERE t."Source" = 0 AND ut."TrackId" = t."Id";
"""); """);
migrationBuilder.Sql(
"""
DELETE FROM "Tracks" WHERE "Source" = 0 AND "OwnerUserId" IS NULL;
""");
migrationBuilder.Sql( migrationBuilder.Sql(
""" """
INSERT INTO "Artists" ("Id", "Name", "NormalizedName", "UserId", "ExternalId", "CreatedAt", "UpdatedAt") INSERT INTO "Artists" ("Id", "Name", "NormalizedName", "UserId", "ExternalId", "CreatedAt", "UpdatedAt")
@@ -120,7 +125,12 @@ namespace Musify.Infrastructure.Persistence.Migrations
""" """
INSERT INTO "Artists" ("Id", "Name", "NormalizedName", "UserId", "ExternalId", "CreatedAt", "UpdatedAt") INSERT INTO "Artists" ("Id", "Name", "NormalizedName", "UserId", "ExternalId", "CreatedAt", "UpdatedAt")
SELECT gen_random_uuid(), s."Artist", upper(s."Artist"), NULL, NULL, now(), now() SELECT gen_random_uuid(), s."Artist", upper(s."Artist"), NULL, NULL, now(), now()
FROM (SELECT DISTINCT "Artist" FROM "Tracks" WHERE "Source" = 1 AND "Artist" IS NOT NULL) s; FROM (
SELECT DISTINCT ON (upper("Artist")) "Artist"
FROM "Tracks"
WHERE "Source" = 1 AND "Artist" IS NOT NULL
ORDER BY upper("Artist"), "Artist"
) s;
"""); """);
migrationBuilder.Sql( migrationBuilder.Sql(
@@ -135,7 +145,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
""" """
INSERT INTO "TrackArtist" ("TrackId", "ArtistId", "Position") INSERT INTO "TrackArtist" ("TrackId", "ArtistId", "Position")
SELECT t."Id", a."Id", 0 FROM "Tracks" t SELECT t."Id", a."Id", 0 FROM "Tracks" t
JOIN "Artists" a ON a."UserId" IS NULL AND a."ExternalId" IS NULL AND a."Name" = t."Artist" JOIN "Artists" a ON a."UserId" IS NULL AND a."ExternalId" IS NULL AND a."NormalizedName" = upper(t."Artist")
WHERE t."Source" = 1 AND t."Artist" IS NOT NULL; WHERE t."Source" = 1 AND t."Artist" IS NOT NULL;
"""); """);