Add anonymous listening support and redesign web-player UI

Backend:
- Expose GET /config/playback so the frontend can detect whether
  anonymous listening is enabled without requiring auth
- Wire anonymous access through track streaming and YouTube resolve flows

Frontend:
- Allow browsing/listening without a session when the backend enables it
- Redesign Inicio, Sidebar and top bar with a new dark theme, typography,
  and layout (top bar with live quick-search, sticky/blurred on scroll)
- Populate Top música, Popular and Recomendados with real Musify +
  YouTube Music data instead of placeholders
- Various shape, spacing, color and animation refinements across the
  redesigned components
This commit is contained in:
2026-09-11 01:29:37 +02:00
parent 10766bbb47
commit 389b3b9244
40 changed files with 1172 additions and 452 deletions
@@ -17,9 +17,6 @@ namespace Musify.Application.YouTube
{
public async ValueTask<ErrorOr<YouTubeAlbumDetail>> Handle(GetYouTubeAlbumQuery request, CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(request.AlbumId))
return Error.Validation(description: "The album id must not be empty.");
var result = await youTubeMusicService.GetAlbumAsync(request.AlbumId, cancellationToken);
if (result.IsError)
return result;
@@ -10,8 +10,6 @@ using Musify.Domain.Entities;
namespace Musify.Application.YouTube
{
/// <param name="UserId">Null for an anonymous request — allowed only when
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
public record ResolveYouTubeTrackStreamCommand(string VideoId, long? UserId)
: ICommand<ErrorOr<YouTubeStreamResponse>>;
@@ -26,7 +24,7 @@ namespace Musify.Application.YouTube
{
public async ValueTask<ErrorOr<YouTubeStreamResponse>> Handle(ResolveYouTubeTrackStreamCommand request, CancellationToken cancellationToken)
{
var isAnonymous = request.UserId is null;
var isAnonymous = !request.UserId.HasValue;
if (isAnonymous && !playbackConfiguration.AllowAnonymousListening)
return Error.Unauthorized(description: "Sign in to stream music, or ask an administrator to enable anonymous listening.");
@@ -43,10 +41,6 @@ namespace Musify.Application.YouTube
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.");
@@ -63,7 +57,7 @@ namespace Musify.Application.YouTube
logger.LogWarning("Could not provision YouTube track {VideoId}: {Error}", request.VideoId, provisionResult.FirstError.Description);
}
if (track != null && request.UserId is not null)
if (track != null&& request.UserId != null)
{
await database.ListeningHistories.AddAsync(new ListeningHistory
{