using ErrorOr; using Mediator; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Musify.Application.Configuration; using Musify.Application.Contracts; using Musify.Application.Services; using Musify.Application.YouTube.Responses; using Musify.Domain.Entities; namespace Musify.Application.YouTube { /// Null for an anonymous request — allowed only when /// is enabled. public record ResolveYouTubeTrackStreamCommand(string VideoId, long? UserId) : ICommand>; public class ResolveYouTubeTrackStreamCommandHandler( IDatabase database, IYouTubeMusicService youTubeMusicService, YouTubeTrackProvisioner provisioner, TrackStreamIssuer streamIssuer, PlaybackConfiguration playbackConfiguration, ILogger logger) : ICommandHandler> { public async ValueTask> Handle(ResolveYouTubeTrackStreamCommand request, CancellationToken cancellationToken) { var isAnonymous = request.UserId is null; if (isAnonymous && !playbackConfiguration.AllowAnonymousListening) return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening."); var track = await provisioner.FindAsync(request.VideoId, cancellationToken); if (track != null && track.Audio.IsProcessed) { var serverStream = await streamIssuer.IssueAsync(track.Id, track.Audio.FolderName, request.UserId, cancellationToken); return new YouTubeStreamResponse( track.Id, YouTubeStreamMode.Server, serverStream.ManifestUrl, serverStream.Ticket, serverStream.ExpiresInSeconds); } // Below this point the only option left is streaming straight from YouTube's own CDN, // which bypasses our Gateway (and its stream tickets) entirely — there's no way to cap that // to the configured anonymous fragment, so an anonymous listener simply doesn't get it: they // can retry once the track has been provisioned server-side, where the cap above applies. if (isAnonymous && playbackConfiguration.AnonymousFragmentSeconds > 0) return Error.Conflict(description: "This track isn't available for anonymous preview yet. Try again in a moment."); var streamInfo = await youTubeMusicService.GetAudioStreamAsync(request.VideoId, cancellationToken); if (streamInfo.IsError) return streamInfo.Errors; if (track == 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 != null && request.UserId is not null) { await database.ListeningHistories.AddAsync(new ListeningHistory { UserId = request.UserId.Value, 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( track?.Id, YouTubeStreamMode.YouTube, streamInfo.Value.Url, null, streamInfo.Value.ExpiresInSeconds); } } }