feat(backend): YouTube Music search and playback resolution endpoints

This commit is contained in:
2026-07-16 01:05:42 +02:00
parent 2cecd118ed
commit 11ed8b40ff
20 changed files with 506 additions and 27 deletions
@@ -0,0 +1,52 @@
using ErrorOr;
using Mediator;
using Microsoft.EntityFrameworkCore;
using Musify.Application.Contracts;
using Musify.Application.Services;
using Musify.Application.YouTube.Responses;
using Musify.Domain.ValueObjects;
namespace Musify.Application.YouTube
{
public record GetYouTubeTrackStreamQuery(string VideoId, long UserId)
: IQuery<ErrorOr<YouTubeStreamResponse>>;
public class GetYouTubeTrackStreamQueryHandler(
IDatabase database,
IYouTubeMusicService youTubeMusicService,
TrackStreamIssuer streamIssuer)
: IQueryHandler<GetYouTubeTrackStreamQuery, ErrorOr<YouTubeStreamResponse>>
{
public async ValueTask<ErrorOr<YouTubeStreamResponse>> Handle(GetYouTubeTrackStreamQuery request, CancellationToken cancellationToken)
{
var track = await database.Tracks.AsNoTracking()
.Where(t => t.Source == TrackSource.YouTube && t.ExternalId == request.VideoId)
.Select(t => new { t.Id, t.AudioTranscodeProcessingStatus, t.AudioFolderName } )
.SingleOrDefaultAsync(cancellationToken);
if (track != null
&& !string.IsNullOrWhiteSpace(track.AudioFolderName)
&& track.AudioTranscodeProcessingStatus == ProcessingStatus.Completed)
{
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;
return new YouTubeStreamResponse(
YouTubeStreamMode.YouTube,
streamInfo.Value.Url,
string.Empty,
streamInfo.Value.ExpiresInSeconds,
Guid.Empty);
}
}
}
@@ -0,0 +1,15 @@
namespace Musify.Application.YouTube.Responses
{
public enum YouTubeStreamMode
{
Server = 0,
YouTube = 1
}
public record YouTubeStreamResponse(
YouTubeStreamMode Mode,
string StreamUrl,
string Ticket,
int ExpiresInSeconds,
Guid TrackId);
}
@@ -0,0 +1,21 @@
using ErrorOr;
using Mediator;
using Musify.Application.Contracts;
namespace Musify.Application.YouTube
{
public record SearchYouTubeTracksQuery(string Query, string ContinuationToken)
: IQuery<ErrorOr<YouTubeSearchResult>>;
public class SearchYouTubeTracksQueryHandler(IYouTubeMusicService youTubeMusicService)
: IQueryHandler<SearchYouTubeTracksQuery, ErrorOr<YouTubeSearchResult>>
{
public async ValueTask<ErrorOr<YouTubeSearchResult>> Handle(SearchYouTubeTracksQuery request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.Query))
return Error.Validation(description: "The search query must not be empty.");
return await youTubeMusicService.SearchSongsAsync(request.Query, request.ContinuationToken, cancellationToken);
}
}
}