refactor(backend): unify YouTube track creation via a provisioner
Track get-or-create for YouTube videos was duplicated between the old GetYouTubeTrackStream query handler (which also wrote to the database) and AddYouTubeTrackToPlayList, with frontend-supplied metadata (title/artist/duration/thumbnail) trusted at add-to-playlist time instead of resolving it server-side. Extract YouTubeTrackProvisioner as the single get-or-create path, resolving structured artist refs (id + name) from YouTube Music so multi-artist songs map to distinct Artist rows. Rename GetYouTubeTrackStream to ResolveYouTubeTrackStream (it writes, so it is a command, not a query) and delegate to the provisioner. AddYouTubeTrackToPlayList slims down to (UserId, PlayListId, VideoId) since all metadata now comes from the provisioner/YouTube API.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
using ErrorOr;
|
||||
using Mediator;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Musify.Application.Contracts;
|
||||
using Musify.Application.Services;
|
||||
using Musify.Application.YouTube.Responses;
|
||||
using Musify.Domain.Entities;
|
||||
|
||||
namespace Musify.Application.YouTube
|
||||
{
|
||||
public record ResolveYouTubeTrackStreamCommand(string VideoId, long UserId)
|
||||
: ICommand<ErrorOr<YouTubeStreamResponse>>;
|
||||
|
||||
public class ResolveYouTubeTrackStreamCommandHandler(
|
||||
IDatabase database,
|
||||
IYouTubeMusicService youTubeMusicService,
|
||||
YouTubeTrackProvisioner provisioner,
|
||||
TrackStreamIssuer streamIssuer,
|
||||
ILogger<ResolveYouTubeTrackStreamCommandHandler> logger)
|
||||
: ICommandHandler<ResolveYouTubeTrackStreamCommand, ErrorOr<YouTubeStreamResponse>>
|
||||
{
|
||||
public async ValueTask<ErrorOr<YouTubeStreamResponse>> Handle(ResolveYouTubeTrackStreamCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await provisioner.FindAsync(request.VideoId, cancellationToken);
|
||||
|
||||
if (track is not null && track.IsAudioProcessed)
|
||||
{
|
||||
var serverStream = await streamIssuer.IssueAsync(track.Id, track.AudioFolderName, request.UserId, cancellationToken);
|
||||
return new YouTubeStreamResponse(
|
||||
YouTubeStreamMode.Server,
|
||||
serverStream.ManifestUrl,
|
||||
serverStream.Ticket,
|
||||
serverStream.ExpiresInSeconds,
|
||||
track.Id);
|
||||
}
|
||||
|
||||
var streamInfo = await youTubeMusicService.GetAudioStreamAsync(request.VideoId, cancellationToken);
|
||||
if (streamInfo.IsError)
|
||||
return streamInfo.Errors;
|
||||
|
||||
if (track is null)
|
||||
{
|
||||
var provisionResult = await provisioner.GetOrCreateAsync(request.VideoId, cancellationToken);
|
||||
if (!provisionResult.IsError)
|
||||
track = provisionResult.Value.Track;
|
||||
else
|
||||
logger.LogWarning("Could not provision YouTube track {VideoId}: {Error}", request.VideoId, provisionResult.FirstError.Description);
|
||||
}
|
||||
|
||||
if (track is not null)
|
||||
{
|
||||
await database.ListeningHistories.AddAsync(new ListeningHistory
|
||||
{
|
||||
UserId = request.UserId,
|
||||
TrackId = track.Id,
|
||||
}, cancellationToken);
|
||||
|
||||
try
|
||||
{
|
||||
await database.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
catch (DbUpdateException exception)
|
||||
{
|
||||
logger.LogWarning(exception, "Failed to record listening history for YouTube video {VideoId}", request.VideoId);
|
||||
}
|
||||
}
|
||||
|
||||
return new YouTubeStreamResponse(
|
||||
YouTubeStreamMode.YouTube,
|
||||
streamInfo.Value.Url,
|
||||
string.Empty,
|
||||
streamInfo.Value.ExpiresInSeconds,
|
||||
track?.Id ?? Guid.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user