feat: recommend server-generated mixes from Musify and YouTube Music

Mixes are their own concept, not playlists: they live in their own
Mixes/MixItems tables, cannot be edited and never show up among the
user's playlists. Each item is either a Musify track or a YouTube Music
reference, so YouTube songs stream directly instead of being downloaded.

A daily Hangfire job seeds the mixes from the listening history: one
discovery mix, one daily mix and one per top artist.

The per-user YouTube filler query was already in the working tree and
rides along here because it shares files with this change.
This commit is contained in:
2026-07-21 18:13:07 +02:00
parent 2cb51f540e
commit 90e842d084
36 changed files with 2667 additions and 13 deletions
@@ -0,0 +1,37 @@
using Mediator;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Musify.Application.Contracts;
using Musify.Application.Mixes;
namespace Musify.Infrastructure.Jobs
{
public class DailyMixGenerationJob(
IDatabase database,
IMediator mediator,
ILogger<DailyMixGenerationJob> logger)
{
public async Task RunAsync(CancellationToken cancellationToken)
{
var userIds = await database.ListeningHistories
.AsNoTracking()
.Select(history => history.UserId)
.Distinct()
.ToListAsync(cancellationToken);
logger.LogInformation("Starting daily mix generation for {Count} users", userIds.Count);
foreach (var userId in userIds)
{
var result = await mediator.Send(new GenerateMixesForUserCommand(userId), cancellationToken);
if (result.IsError)
{
logger.LogWarning("Failed to generate mixes for user {UserId}: {Error}",
userId, result.FirstError.Description);
}
}
logger.LogInformation("Finished daily mix generation");
}
}
}