diff --git a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs index f2ef0d4..b516170 100644 --- a/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs +++ b/backend/Musify.Application/Services/YouTubeTrackProvisioner.cs @@ -8,7 +8,7 @@ using Musify.Domain.ValueObjects; 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( IDatabase database, @@ -26,7 +26,7 @@ public sealed class YouTubeTrackProvisioner( { var existing = await FindAsync(videoId, cancellationToken); 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); if (songResult.IsError) @@ -81,7 +81,7 @@ public sealed class YouTubeTrackProvisioner( { await database.SaveChangesAsync(cancellationToken); 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) { @@ -92,7 +92,7 @@ public sealed class YouTubeTrackProvisioner( 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 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) 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 { Name = name, diff --git a/backend/Musify.Application/Users/SyncUser.cs b/backend/Musify.Application/Users/SyncUser.cs index 576d149..2b12afb 100644 --- a/backend/Musify.Application/Users/SyncUser.cs +++ b/backend/Musify.Application/Users/SyncUser.cs @@ -47,6 +47,14 @@ namespace Musify.Application.Users user.SecondName = request.SecondName; 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); if (written > 0) logger.LogInformation("Synced profile for user {UserId}", request.Id); diff --git a/backend/Musify.Infrastructure/Persistence/Migrations/20260716214752_AddArtistsAndTrackOwnership.cs b/backend/Musify.Infrastructure/Persistence/Migrations/20260716214752_AddArtistsAndTrackOwnership.cs index 7dd574d..47d18d9 100644 --- a/backend/Musify.Infrastructure/Persistence/Migrations/20260716214752_AddArtistsAndTrackOwnership.cs +++ b/backend/Musify.Infrastructure/Persistence/Migrations/20260716214752_AddArtistsAndTrackOwnership.cs @@ -108,6 +108,11 @@ namespace Musify.Infrastructure.Persistence.Migrations WHERE t."Source" = 0 AND ut."TrackId" = t."Id"; """); + migrationBuilder.Sql( + """ + DELETE FROM "Tracks" WHERE "Source" = 0 AND "OwnerUserId" IS NULL; + """); + migrationBuilder.Sql( """ 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") 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( @@ -135,7 +145,7 @@ namespace Musify.Infrastructure.Persistence.Migrations """ INSERT INTO "TrackArtist" ("TrackId", "ArtistId", "Position") 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; """);