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 637e1ae583
commit 496a5f4a0b
36 changed files with 2667 additions and 13 deletions
+13
View File
@@ -15,6 +15,7 @@
import { hueFor } from '$lib/theme/color';
import Cover from '$lib/components/ui/Cover.svelte';
import PlaylistCard from '$lib/components/ui/PlaylistCard.svelte';
import MixCard from '$lib/components/ui/MixCard.svelte';
import NowPlaying from '$lib/components/ui/NowPlaying.svelte';
import EmptyState from '$lib/components/ui/EmptyState.svelte';
import ExplicitBadge from '$lib/components/ui/ExplicitBadge.svelte';
@@ -106,6 +107,7 @@
}
}
const playlists = $derived(data.playlists);
const mixes = $derived(data.mixes);
const trackIds = $derived(data.trackIds);
function playLatest(index: number) {
@@ -224,6 +226,17 @@
</section>
{/if}
{#if mixes.length > 0}
<section class="page-x pt-5 pb-3 sm:pt-6">
<SectionHeading title="Hechas para ti" />
<MediaGrid>
{#each mixes as mix, i (mix.id)}
<MixCard {mix} index={i} />
{/each}
</MediaGrid>
</section>
{/if}
{#if playlists.length > 0}
<section class="page-x pt-5 pb-3 sm:pt-6">
<SectionHeading title="Mis listas" />