Code review of the Artist/ownership refactor surfaced three gaps: - SyncUser updated User.Name on every login but never the linked Artist row, so renames never reached track credits - defeating the point of centralizing names in Artist. - YouTubeTrackProvisioner looked up artists by ExternalId only when a browseId was known, so it could never reuse the ExternalId-less rows the migration backfilled from legacy Artist strings, creating duplicate Artist rows for the same person. It now adopts the backfilled row (assigning it the browseId) when one matches by normalized name. - AddArtistsAndTrackOwnership deduped legacy YouTube artist strings case-sensitively before hashing them into a case-insensitive unique index, and never guarded against local tracks with no UserHasTrack row - either would abort the migration on real data. Backfill now dedupes via DISTINCT ON over the normalized name and drops orphaned local tracks before the ownership check constraint is added.
66 lines
2.3 KiB
C#
66 lines
2.3 KiB
C#
using ErrorOr;
|
|
using Mediator;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Logging;
|
|
using Musify.Application.Contracts;
|
|
using Musify.Domain.Entities;
|
|
|
|
namespace Musify.Application.Users
|
|
{
|
|
public record SyncUserCommand(
|
|
long Id,
|
|
string Name,
|
|
string? FirstName,
|
|
string? SecondName,
|
|
string? ProfilePictureUrl)
|
|
: ICommand<ErrorOr<Success>>;
|
|
|
|
public class SyncUserCommandHandler(IDatabase database, ILogger<SyncUserCommandHandler> logger)
|
|
: ICommandHandler<SyncUserCommand, ErrorOr<Success>>
|
|
{
|
|
public async ValueTask<ErrorOr<Success>> Handle(SyncUserCommand request, CancellationToken cancellationToken)
|
|
{
|
|
var user = await database.Users
|
|
.SingleOrDefaultAsync(u => u.Id == request.Id, cancellationToken);
|
|
|
|
if (user is null)
|
|
{
|
|
await database.Users.AddAsync(new User
|
|
{
|
|
Id = request.Id,
|
|
Name = request.Name,
|
|
NormalizedName = request.Name.ToUpperInvariant(),
|
|
FirstName = request.FirstName,
|
|
SecondName = request.SecondName,
|
|
ProfilePictureUrl = request.ProfilePictureUrl
|
|
}, cancellationToken);
|
|
|
|
await database.SaveChangesAsync(cancellationToken);
|
|
logger.LogInformation("Provisioned user {UserId}", request.Id);
|
|
|
|
return new Success();
|
|
}
|
|
|
|
user.Name = request.Name;
|
|
user.NormalizedName = request.Name.ToUpperInvariant();
|
|
user.FirstName = request.FirstName;
|
|
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);
|
|
|
|
return new Success();
|
|
}
|
|
}
|
|
}
|