Design Prot Merge #1

Merged
NotS1mp merged 217 commits from design-prot into master 2026-09-21 15:48:16 +00:00
99 changed files with 381 additions and 578 deletions
Showing only changes of commit 2662abe460 - Show all commits
@@ -1,6 +1,6 @@
namespace Musify.Application.Albums.Responses
{
/// <summary>A page of the user's catalog albums (their own).</summary>
public record AlbumsSearchResponse(
IReadOnlyList<AlbumSearchItemResponse> Items,
int PageNumber,
@@ -3,8 +3,7 @@ using Musify.Application.Configuration.Responses;
namespace Musify.Application.Configuration
{
/// <summary>Reads the parts of <see cref="PlaybackConfiguration"/> that are safe to expose to an
/// unauthenticated client, so it can decide whether to gate the app behind sign-in at all.</summary>
public record GetPlaybackPublicConfigQuery : IQuery<PlaybackPublicConfigResponse>;
public class GetPlaybackPublicConfigQueryHandler(PlaybackConfiguration playbackConfiguration)
@@ -2,28 +2,16 @@ using System.ComponentModel.DataAnnotations;
namespace Musify.Application.Configuration
{
/// <summary>Controls whether listeners without a session can stream music at all, and if so,
/// how much of a track they get to hear before being cut off (a Spotify-style preview).</summary>
public sealed class PlaybackConfiguration
{
public const string SectionName = "Playback";
/// <summary>When false (the default), every stream endpoint requires a signed-in user, exactly
/// like before this setting existed. When true, an anonymous request is issued a stream ticket
/// too, capped by <see cref="AnonymousFragmentSeconds"/> if that is greater than zero.</summary>
public bool AllowAnonymousListening { get; set; }
/// <summary>How many seconds of a track an anonymous listener may hear, enforced by the
/// Streaming Gateway clamping the byte range it will serve for that ticket. Zero (the default)
/// means no cap: anonymous listeners hear the full track, same as a signed-in user. Ignored
/// when <see cref="AllowAnonymousListening"/> is false.</summary>
[Range(0, 3600)]
public int AnonymousFragmentSeconds { get; set; }
/// <summary>Bytes per second of the transcoded audio, used to turn <see cref="AnonymousFragmentSeconds"/>
/// into a byte cap for the stream ticket. Defaults to the app's own transcoder output (128 kbps
/// AAC, <c>AudioTranscoder:Ffmpeg:AudioBitrate</c>) — update this alongside that setting if it
/// ever changes, since the two aren't wired together.</summary>
[Range(1, int.MaxValue)]
public int EstimatedAudioBytesPerSecond { get; set; } = 16_000;
}
@@ -1,7 +1,5 @@
namespace Musify.Application.Configuration.Responses
{
/// <param name="AllowAnonymousListening">Whether listeners without a session can stream music at all.</param>
/// <param name="AnonymousFragmentSeconds">How many seconds of a track an anonymous listener may hear.
/// Zero means no cap. Meaningless when <paramref name="AllowAnonymousListening"/> is false.</param>
public record PlaybackPublicConfigResponse(bool AllowAnonymousListening, int AnonymousFragmentSeconds);
}
@@ -2,11 +2,7 @@ namespace Musify.Application.Contracts
{
public interface IStreamTicketService
{
/// <param name="userId">Null for an anonymous listener — the ticket is issued without a
/// <c>sub</c> claim.</param>
/// <param name="keyPrefix">The storage key (prefix) the ticket authorizes reading from.</param>
/// <param name="maxBytes">When set, the Streaming Gateway clamps the byte range it will serve
/// for this ticket to <c>[0, maxBytes)</c> — used for the anonymous preview fragment.</param>
StreamTicket IssueTicket(long? userId, string keyPrefix, long? maxBytes = null);
}
@@ -12,11 +12,6 @@ namespace Musify.Application.Mixes
public record GenerateMixesForUserCommand(long UserId)
: ICommand<ErrorOr<Success>>;
/// <summary>Builds each user's mixes purely from their own library: a "Descubrimiento" mix of
/// tracks they own but haven't listened to yet, and a "Tu mezcla diaria" mix blending recently
/// listened tracks with unheard ones. There's no artist/genre metadata for local uploads today,
/// so mixes can't be seeded by "similar artist" the way the old YouTube-backed discovery did —
/// once a tagging system exists, this is the place to seed mixes by tag instead.</summary>
public class GenerateMixesForUserCommandHandler(
IDatabase database,
MixConfiguration mixConfiguration,
@@ -5,9 +5,7 @@ using Musify.Domain.Entities;
namespace Musify.Application.Mixes
{
/// <summary>Resolves MixItem rows to their MixItemApplicationResponse shape via a single batched
/// track lookup, instead of an EF Include chain deep enough (Mix -&gt; Items -&gt; Track -&gt; Owner) to
/// force a correlated-subquery translation some providers (SQLite, notably) can't produce.</summary>
internal static class MixItemMapper
{
public static async Task<List<MixItemApplicationResponse>> ToResponsesAsync(
@@ -4,13 +4,6 @@ using System.Text.Json.Serialization;
namespace Musify.Application.Serialization;
// Zitadel-issued user ids routinely exceed Number.MAX_SAFE_INTEGER (2^53-1).
// Serialized as a raw JSON number, the frontend's JSON.parse silently rounds
// them to the nearest representable double, corrupting the id (it becomes a
// different, sometimes valid, user id). Applied via [property: JsonConverter(...)]
// on the specific id-shaped `long`/`long?` response properties (not globally —
// unrelated `long` fields like byte sizes must stay numbers), this serializes
// them as a JSON string instead, which round-trips exactly.
public sealed class LongAsStringConverter : JsonConverter<long>
{
public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
@@ -30,8 +30,6 @@ public sealed class TrackStreamIssuer(
folderPath,
streamGatewayConfiguration.AudioFileName);
// Anonymous listeners have no User row to satisfy ListeningHistory's foreign key against, and
// their plays aren't "history" anyone can look back on anyway, so there's nothing to record.
if (userId != null)
{
var newListeningHistory = new ListeningHistory
@@ -9,8 +9,7 @@ using Musify.Application.Tracks.Responses;
namespace Musify.Application.Tracks
{
/// <param name="UserId">Null for an anonymous request — allowed only when
/// <see cref="PlaybackConfiguration.AllowAnonymousListening"/> is enabled.</param>
public record GetTrackStreamQuery(Guid TrackId, long? UserId)
: IQuery<ErrorOr<TrackStreamResponse>>;
@@ -1,6 +1,6 @@
namespace Musify.Application.Tracks.Responses
{
/// <summary>A page of the user's catalog tracks (their own uploads).</summary>
public record TracksSearchResponse(
IReadOnlyList<TrackSearchItemResponse> Items,
int PageNumber,
@@ -1,9 +1,6 @@
namespace Musify.Domain.ValueObjects
{
/// <summary>
/// Encodes which genre pairs are mutually exclusive on a single track (e.g. a track cannot
/// be tagged as both Classical and Metal at the same time).
/// </summary>
public static class GenreCompatibility
{
private static readonly HashSet<(Genre, Genre)> IncompatiblePairs = BuildIncompatiblePairs();
@@ -31,9 +28,6 @@ namespace Musify.Domain.ValueObjects
public static bool AreCompatible(Genre a, Genre b) =>
a == b || !IncompatiblePairs.Contains((a, b));
/// <summary>
/// Returns every mutually exclusive genre pair found within the given set of tags.
/// </summary>
public static IReadOnlyCollection<(Genre First, Genre Second)> FindConflicts(IEnumerable<Genre> tags)
{
var distinctTags = tags.Distinct().ToList();
@@ -24,11 +24,6 @@ namespace Musify.Infrastructure.Persistence
if (context == null)
return;
// SavingChanges(Async) fires before EF's own automatic change detection for this
// save, so entities modified through plain property assignment (query, then set a
// property, then SaveChanges — the normal case) still show as Unchanged here unless
// something already forced detection. Force it explicitly, otherwise UpdatedAt is
// never bumped on a real update, only on inserts.
context.ChangeTracker.DetectChanges();
var now = DateTime.UtcNow;
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260531234306_AddUserProfilePictureUrl")]
partial class AddUserProfilePictureUrl
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserProfilePictureUrl : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260601195757_RenameTimestampsAndAudit")]
partial class RenameTimestampsAndAudit
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RenameTimestampsAndAudit : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
newName: "CreatedAt");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705123932_AddTrackProcessingStateSaga")]
partial class AddTrackProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705124807_AddTrackProcessingSagaKeys")]
partial class AddTrackProcessingSagaKeys
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackProcessingSagaKeys : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -29,7 +29,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260705125356_AddPlayListProcessingStateSaga")]
partial class AddPlayListProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPlayListProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260713234847_AddListeningHistory")]
partial class AddListeningHistory
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddListeningHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -47,7 +47,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260714114537_AddTrackDuration")]
partial class AddTrackDuration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackDuration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260715193951_AddTrackSourceArtistExternalId")]
partial class AddTrackSourceArtistExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackSourceArtistExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
filter: "\"ExternalId\" <> ''");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260715230836_AddTrackDownloadRequested")]
partial class AddTrackDownloadRequested
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackDownloadRequested : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: false);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716214442_MakeTrackMediaAndExternalIdNullable")]
partial class MakeTrackMediaAndExternalIdNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakeTrackMediaAndExternalIdNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -66,7 +66,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716214752_AddArtistsAndTrackOwnership")]
partial class AddArtistsAndTrackOwnership
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddArtistsAndTrackOwnership : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<long>(
@@ -154,7 +154,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
sql: "(\"OwnerUserId\" IS NOT NULL) = (\"Source\" = 0)");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260716220047_DropTrackArtistColumn")]
partial class DropTrackArtistColumn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DropTrackArtistColumn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
table: "Tracks");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717164632_MovePlayListPicturesToOwnedType")]
partial class MovePlayListPicturesToOwnedType
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,16 +4,15 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MovePlayListPicturesToOwnedType : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717164812_RenameTrackDurationToDurationSeconds")]
partial class RenameTrackDurationToDurationSeconds
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RenameTrackDurationToDurationSeconds : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -16,7 +16,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
newName: "DurationSeconds");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717172507_SplitTrackIntoLocalAndExternal")]
partial class SplitTrackIntoLocalAndExternal
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class SplitTrackIntoLocalAndExternal : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -156,7 +156,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldNullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260717223358_MakePlayListPicturesNullable")]
partial class MakePlayListPicturesNullable
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListPicturesNullable : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -51,7 +51,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldMaxLength: 64);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720180629_WidenTrackTitle")]
partial class WidenTrackTitle
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class WidenTrackTitle : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -31,7 +31,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
oldMaxLength: 50);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720180747_AddAlbums")]
partial class AddAlbums
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAlbums : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -100,7 +100,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "OwnerUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260720184656_AddUserAlbumExternalId")]
partial class AddUserAlbumExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserAlbumExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721155102_AddMixes")]
partial class AddMixes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddMixes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -83,7 +83,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "TrackId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721161653_RemoveUserAlbumExternalId")]
partial class RemoveUserAlbumExternalId
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveUserAlbumExternalId : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -24,7 +24,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
column: "OwnerUserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721162102_DropMixGeneratedOn")]
partial class DropMixGeneratedOn
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DropMixGeneratedOn : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -15,7 +15,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
table: "Mixes");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<DateOnly>(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260721190735_MakePlayListDescriptionOptional")]
partial class MakePlayListDescriptionOptional
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class MakePlayListDescriptionOptional : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -27,7 +27,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
""");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterColumn<string>(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724170051_DurationAsDouble")]
partial class DurationAsDouble
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,16 +4,15 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class DurationAsDouble : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724181012_AddExternalAlbums")]
partial class AddExternalAlbums
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddExternalAlbums : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -36,7 +36,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260724182659_AddExternalAlbumThumbnail")]
partial class AddExternalAlbumThumbnail
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddExternalAlbumThumbnail : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260910125022_AddAlbumProcessingStateSaga")]
partial class AddAlbumProcessingStateSaga
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddAlbumProcessingStateSaga : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -28,7 +28,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914144913_AddPlayListVisibility")]
partial class AddPlayListVisibility
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -4,10 +4,10 @@
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddPlayListVisibility : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<int>(
@@ -18,7 +18,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
defaultValue: 0);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914150045_AddTrackLikes")]
partial class AddTrackLikes
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackLikes : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260914151900_AddUserFollows")]
partial class AddUserFollows
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,10 +5,10 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddUserFollows : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
@@ -49,7 +49,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -15,7 +15,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260915165436_RemoveYouTubeIntegration")]
partial class RemoveYouTubeIntegration
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -5,25 +5,16 @@ using Microsoft.EntityFrameworkCore.Migrations;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class RemoveYouTubeIntegration : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
// Data cleanup first, while the External*/Local*/UserAlbums tables (and the loose
// MixItem columns) still exist to query/cascade from. Deleting the base Tracks/Albums
// rows for anything that only ever existed as an ExternalTrack/ExternalAlbum cascades
// (FKs are ON DELETE CASCADE) to their own ExternalTracks/ExternalAlbums row, TrackArtist,
// PlayListHasTrack, AlbumHasTrack, TrackLike, ListeningHistory, UserHasTrack and MixItems
// rows that referenced them — this is deliberately lossy: those rows only existed to back
// the YouTube integration being removed here.
migrationBuilder.Sql("""DELETE FROM "Tracks" WHERE "Id" IN (SELECT "Id" FROM "ExternalTracks");""");
migrationBuilder.Sql("""DELETE FROM "Albums" WHERE "Id" IN (SELECT "Id" FROM "ExternalAlbums");""");
// Live YouTube search hits that were seeded into a mix but never materialized into a
// real track (MixItemSource.YouTube, no TrackId) — MixItem.TrackId is about to become
// required, so these rows can't be kept.
migrationBuilder.Sql("""DELETE FROM "MixItems" WHERE "TrackId" IS NULL;""");
migrationBuilder.AddColumn<long>(
@@ -132,7 +123,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
onDelete: ReferentialAction.Restrict);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -16,7 +16,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
[Migration("20260916153700_AddTrackTags")]
partial class AddTrackTags
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
@@ -6,10 +6,10 @@ using Musify.Domain.ValueObjects;
namespace Musify.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class AddTrackTags : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
@@ -41,7 +41,6 @@ namespace Musify.Infrastructure.Persistence.Migrations
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
@@ -1,4 +1,4 @@
// <auto-generated />

using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
@@ -41,13 +41,8 @@ public sealed class JwtBearerEventsHandler(
if (cache.TryGetValue(cacheKey, out _))
return;
// Access tokens from the IdP are minimal (just sub/scope/aud) — profile attributes live on
// the userinfo endpoint, so ask for them there instead of relying on token claims alone.
var userInfo = await FetchUserInfoAsync(tokenValidatedContext, tokenValidatedContext.HttpContext.RequestAborted);
// Zitadel also issues short-form JWT claim names ("name", "given_name", "family_name", "email"),
// but the newer JsonWebTokenHandler (default since .NET 8) no longer auto-maps those to the
// legacy ClaimTypes.* URIs the way JwtSecurityTokenHandler used to — so check both forms too.
var username = userInfo?.Name
?? userInfo?.PreferredUsername
?? principal.FindFirstValue("name")
@@ -39,13 +39,6 @@ public sealed class OpenApiOptionsSetup(IOptions<AuthenticationConfiguration> op
return Task.CompletedTask;
} );
// Properties annotated with [JsonConverter(typeof(LongAsStringConverter))] (id-shaped
// `long` fields — see that converter for why) serialize as a JSON string, but the
// schema generator still reflects the declared CLR `long`/`long?` type as a bare
// int64 number. Reconcile the declared schema so generated clients (openapi-typescript)
// type exactly those properties as `string`, matching what actually goes over the wire —
// scoped to the attribute, not the CLR type, so unrelated `long` fields (byte sizes, …)
// are left alone.
options.AddSchemaTransformer((schema, context, _) =>
{
if (context.JsonPropertyInfo?.CustomConverter is LongAsStringConverter or NullableLongAsStringConverter)
-2
View File
@@ -48,6 +48,4 @@ app
app.Run();
// Makes the top-level-statements Program class public so
// WebApplicationFactory<Program> (Musify.Api.Tests) can see it.
public partial class Program;
@@ -6,10 +6,6 @@ using Musify.StreamingGateway.Configuration;
namespace Musify.StreamingGateway.Authentication;
/// <param name="Prefix">The storage key prefix the ticket authorizes reading from. Empty means the
/// ticket was missing, malformed, expired, or otherwise invalid.</param>
/// <param name="MaxBytes">When set, the caller is limited to the first <c>MaxBytes</c> bytes of the
/// object — the anonymous preview fragment cap carried by the <c>maxBytes</c> claim.</param>
public sealed record TicketValidationResult(string Prefix, long? MaxBytes)
{
public static readonly TicketValidationResult Invalid = new(string.Empty, null);
@@ -72,12 +72,6 @@ public sealed class TicketValidationMiddleware(
await next(context);
}
/// <summary>Rewrites the request's <c>Range</c> header so nothing past <paramref name="maxBytes"/>
/// is ever asked of the filer — this is what actually enforces the anonymous preview length,
/// downstream of the ticket carrying that cap. A request with no <c>Range</c> header (a plain,
/// whole-file GET) gets one synthesized, so the filer always responds with a bounded 206 instead of
/// streaming the full object. Returns false when the requested range starts at or past the cap, in
/// which case the caller should answer 416 directly instead of proxying anything.</summary>
private static bool TryClampRange(HttpContext context, long maxBytes)
{
var rangeHeader = context.Request.Headers.Range;
-5
View File
@@ -7,11 +7,6 @@ using Musify.Infrastructure.Services;
var builder = Host.CreateApplicationBuilder();
// Registering Mediator pulls in every handler in Musify.Application, including ones
// (e.g. CreatePlayList, CreateTrack) that depend on IEventBus, which the Worker never
// registers because it only consumes events, it never publishes them. Only the handlers
// this process actually dispatches (GenerateMixesForUserCommand) are ever constructed,
// so the default eager validate-on-build check is too strict for this host.
builder.ConfigureContainer(new DefaultServiceProviderFactory(new ServiceProviderOptions
{
ValidateOnBuild = false,
+1 -23
View File
@@ -1,21 +1,4 @@
#!/usr/bin/env node
// Guardrail contra la reintroducción de lo que P0-P14 limpió
// (ver docs/frontend-refactor-plan.md, secciones B.4 y P14). Escanea
// src/**/*.svelte en busca de valores arbitrarios y utilidades de Tailwind
// que deberían ser tokens del sistema de diseño (src/routes/layout.css).
//
// Gaps conocidos, no cubiertos aquí a propósito:
// - `strokeWidth={n}` en iconos: hay ~13 valores distintos repartidos por
// casi todos los componentes. Migrarlo a las recetas `stroke-*` requeriría
// verificar que lucide-svelte respeta `stroke-width` por CSS en vez del
// atributo del componente, y no hay forma de comprobarlo visualmente en
// esta sesión (sin Docker). Se deja fuera para no arriesgar una regresión
// silenciosa en todos los iconos de la app.
// - `text-base` sin receta: solo queda un caso real (Queue.svelte, título
// "En cola", 16px) sin equivalente en la escala text-display-*. Crear una
// receta nueva para un único consumidor sería una abstracción prematura;
// se acepta como excepción documentada en vez de forzarlo a un tamaño que
// no le corresponde.
import { readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
@@ -24,7 +7,6 @@ const files = execSync('git ls-files "src/**/*.svelte"', { encoding: 'utf8' })
.split('\n')
.filter(Boolean);
// B.4 — arbitrarios permitidos tras el refactor.
const ARBITRARY_WHITELIST = [
/^text-\[clamp\([^)]+\)\]$/, // tipografía fluida (login, Logo)
/^leading-\[[\d.]+\]$/, // leading ajustado del display gigante de login
@@ -33,13 +15,9 @@ const ARBITRARY_WHITELIST = [
/^my-\[\d+d?vh\]$/, // +error.svelte: centrado vertical de página de error
/^py-\[\d+d?vh\]$/, // Explorar: centrado vertical de "sin resultados"
/^transition-\[filter\]$/, // Explorar: genre-tile hover
/^bg-\[image:var\(--mf-[\w-]+\)\]$/ // gradientes dinámicos por tema (Artwork, Logo, liked)
/^bg-\[image:var\(--mf-[\w-]+\)\]$/
];
// Radios semánticos de B.1.1 — cualquier otro rounded-* de Tailwind (o el
// `rounded` a secas de fábrica) es una regresión. `rounded-full` y los
// tokens (`rounded-tag/thumb/control/art/art-lg/panel/panel-lg`) están bien:
// el lookahead exige que no venga más texto de clase pegado con `-`.
const RAW_ROUNDED = /\brounded(-(none|xs|sm|md|lg|xl|2xl|3xl))?(?![\w-])/g;
const PAIRED_HW = /\b(?:([a-z0-9-]+:))?h-([\d.]+)\s+\1w-\2\b/g;
+239 -242
View File
File diff suppressed because it is too large Load Diff
@@ -109,7 +109,7 @@
<div class="mt-1 flex flex-col gap-0.5">
{#each tracks as track, i (track.id)}
{@const active = player.current.id === track.id}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="group grid track-grid items-center gap-3 rounded-control py-3 pr-4 pl-2 transition-colors sm:gap-8 sm:pr-6 sm:pl-3 {active
? 'bg-accent-tint'
@@ -132,7 +132,7 @@
{/if}
</button>
{/if}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div
role="button"
tabindex="0"
-2
View File
@@ -1,5 +1,3 @@
// No hay sistema de tags/géneros todavía: esta lista es un placeholder de
// cliente y cada tile enlaza a una búsqueda normal por ese término.
export const GENRE_INFO: Record<string, { label: string; tagline: string }> = {
pop: { label: 'Pop', tagline: 'Éxitos que suenan en todas partes' },
rock: { label: 'Rock', tagline: 'Guitarras, actitud y ruido' },
-4
View File
@@ -1,9 +1,5 @@
import { player, toQueueItems, type ApiTrackLike, type QueueItem } from '$lib/player/player.svelte';
/**
* Merges the in-memory session history (tracks played since the page loaded)
* with the server-side listening history, de-duplicated by id and capped at `limit`.
*/
export function mergeRecentlyPlayed(history: ApiTrackLike[], limit: number): QueueItem[] {
const seen = new Set<string>();
const merged: QueueItem[] = [];
-4
View File
@@ -88,10 +88,6 @@ export interface Birthday {
day: number;
}
/**
* `birthday` is optional because the user's date of birth isn't part of the
* session/profile data yet. Wire it up here once that field exists upstream.
*/
export function pickGreeting(date: Date, birthday?: Birthday | null): string {
const month = date.getMonth() + 1;
const day = date.getDate();
@@ -15,8 +15,6 @@ export async function getAllowAnonymousListening(fetchFn: typeof fetch): Promise
cached = { allowAnonymousListening, expiresAt: Date.now() + CACHE_TTL_MS };
return allowAnonymousListening;
} catch {
// The API being unreachable shouldn't take down every page in the app;
// fail closed (as if anonymous listening were off) and retry next time.
return false;
}
}
-12
View File
@@ -1,10 +1,4 @@
:root {
/* Hue-based tokens (--mf-accent*, --mf-bg/elevated/panel-bg/bar-bg/hairline,
--mf-ambient, --mf-hero-bg/spotlight-bg/modal-glow, --mf-logo-grad,
--mf-cover-grad) are NOT defined here: they come only from
buildThemeTokens() in theme/tokens.ts, inlined as SSR <style> by
+layout.svelte and reapplied on every hue change. A duplicate default
here could never be overridden by a light-mode selector (see A.2). */
--mf-liked-grad: linear-gradient(150deg, oklch(0.4 0.09 330), oklch(0.18 0.04 10));
--mf-accent-dim: color-mix(in oklch, var(--mf-accent) 22%, transparent);
--mf-tile-l: 0.34;
@@ -12,7 +6,6 @@
--mf-tile-shade-l: 0.14;
--mf-tile-shade-c: 0.035;
/* Fixed neutrals: deliberately hue-less, so only the accent carries color. */
--mf-text: #d9d8dc;
--mf-text-2: #8a8a8e;
--mf-text-3: #7d7d81;
@@ -62,14 +55,12 @@
--z-grain: 80;
}
/* PlayerBar switches from its compact row to the full layout at `sm` (640px). */
@media (width >= 40rem) {
:root {
--mf-player-h: 100px;
}
}
/* TabsBar (mobile/tablet nav) is replaced by the Sidebar at `lg` (1024px). */
@media (width >= 64rem) {
:root {
--mf-nav-h: 0px;
@@ -82,9 +73,6 @@
}
}
/* "Modo blanco" — flips the neutral canvas/surface/text tokens to a light
palette. Components that reference these tokens re-theme correctly;
components hardcoding literal dark-mode hex colors do not. */
:root[data-theme='light'] {
--mf-text: #17171a;
--mf-text-2: #4a4a4e;
-18
View File
@@ -13,10 +13,6 @@ function tc(alpha: number, l: number, hue: number): string {
return `oklch(${r(lightness)} ${r(chroma)} ${r(hue, 1)} / ${alpha})`;
}
// Light-mode counterpart of tc(): fades toward a soft tinted glass (~0.88
// lightness) instead of toward black, so the "glow" gradients read as a
// visible colored tint instead of a dark smudge — or, at the old lightness
// floor of 0.97, an almost invisible wash next to the near-white canvas.
function tcLight(alpha: number, l: number, hue: number): string {
const depth = (88 - l) / 58;
const lightness = 0.88 - 0.16 * depth;
@@ -28,15 +24,6 @@ export interface ThemeTokens {
[cssVar: string]: string;
}
/**
* Every color in the app derives from the hue of the track in playback.
* See design_handoff_musify_player/README.md § "Sistema de color dinámico".
*
* `mode` only affects the neutral canvas tokens (bg/elevated/panel/hairline):
* these are re-applied as inline styles on every hue change, so a light-mode
* override in theme.css alone can never win against them — the light values
* have to come from here instead.
*/
export function buildThemeTokens(hue: number, mode: 'dark' | 'light' = 'dark'): ThemeTokens {
const h = ((hue % 360) + 360) % 360;
const h2 = (h + 40) % 360;
@@ -44,11 +31,6 @@ export function buildThemeTokens(hue: number, mode: 'dark' | 'light' = 'dark'):
const H2 = r(h2, 1);
const light = mode === 'light';
// Dark mode wants a bright, light accent (pops on a near-black canvas).
// Light mode needs the opposite: the same hue pulled down to a dark,
// saturated shade so it still reads against a near-white canvas — using
// the dark-mode lightness here is what made accent text/icons/buttons
// nearly invisible in light mode.
const accentL = light ? 0.52 : L;
const accentC = light ? 0.16 : C;
const titleL = light ? 0.4 : 0.87;
@@ -11,8 +11,6 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const query = url.searchParams.get('q')?.trim() ?? '';
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
// This page is reachable without a session when anonymous listening is on
// (see +layout.server.ts), so `user` may be null here.
const user = locals.user;
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
@@ -273,7 +273,6 @@
<PlayButton as="span" size="lg" label="Reproducir" class="group-hover/top:brightness-110" />
{/snippet}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<svelte:element
this={topResult.kind === 'track' ? 'button' : 'a'}
type={topResult.kind === 'track' ? 'button' : undefined}
@@ -308,7 +307,6 @@
{/if}
{#if !data.query}
<!-- GÉNEROS -->
<div class="grid-wide">
{#each genreTiles as genre, i (genre.query)}
<a
-1
View File
@@ -23,7 +23,6 @@ async function fetchSidebarPlaylists(
});
return data?.items ?? [];
} catch {
// The sidebar shell must render even when the API is unreachable.
return [];
}
}
-1
View File
@@ -11,7 +11,6 @@ import {
import { pickGreeting } from '$lib/server/greeting';
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
// This dashboard is personal; an anonymous listener goes to /explore instead.
if (!locals.user) redirect(302, '/explore');
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
-5
View File
@@ -182,7 +182,6 @@
</div>
<div class="mt-10 flex flex-col gap-section">
<!-- CONTINUAR ESCUCHANDO -->
<section>
<SectionHeading title="Continuar escuchando" subtitle="Retomalo donde lo dejaste" />
{#if continueItems.length > 0}
@@ -209,7 +208,6 @@
{/if}
</section>
<!-- PLAYLIST DESTACADA -->
{#if spotlight}
<section>
<div
@@ -274,7 +272,6 @@
</section>
{/if}
<!-- MIXES -->
<section>
<SectionHeading title="Mixes" subtitle="Generados a partir de lo que más repites" />
{#if mixes.length > 0}
@@ -299,7 +296,6 @@
{/if}
</section>
<!-- POPULARES ESTA SEMANA -->
<section>
<SectionHeading title="Populares esta semana" subtitle="Lo más escuchado de tu biblioteca" />
{#if popularTracks.length > 0}
@@ -339,7 +335,6 @@
{/if}
</section>
<!-- TUS PLAYLISTS -->
<section>
<SectionHeading
title="Tus playlists"
@@ -3,8 +3,6 @@ import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
// No hard login gate here: the API itself allows an anonymous request when
// AllowAnonymousListening is on, and rejects it with 401 otherwise.
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const result = await api.GET('/tracks/{id}/stream', {
params: { path: { id: params.id } }