Compare commits
18
Commits
d892d976a3
...
2662abe460
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2662abe460 | ||
|
|
1b6d7ecb42 | ||
|
|
b60a87adb5 | ||
|
|
5fa901c2e6 | ||
|
|
8975e43729 | ||
|
|
1bdd7ec9f1 | ||
|
|
964dfc809f | ||
|
|
8cea298b2a | ||
|
|
fb4a8ccc0e | ||
|
|
14ee1dc45d | ||
|
|
1320b12699 | ||
|
|
aa20e70671 | ||
|
|
a4ca1f32d3 | ||
|
|
58eb670fed | ||
|
|
7b1b81c45a | ||
|
|
ea9bf07126 | ||
|
|
11789e78fe | ||
|
|
96c75a32c8 |
@@ -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,
|
||||
|
||||
@@ -77,8 +77,6 @@ namespace Musify.Application.Albums
|
||||
finalPictureKey = albumConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
}
|
||||
|
||||
database.Albums.Update(album);
|
||||
|
||||
if (pictureIntent != null && finalPictureKey != null)
|
||||
{
|
||||
var publishResult = await PublishUpdateAlbumPictureSourceEventAsync(
|
||||
|
||||
@@ -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
-3
@@ -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 -> Items -> Track -> 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(
|
||||
|
||||
@@ -106,7 +106,7 @@ namespace Musify.Application.PlayLists
|
||||
}, validation.Value);
|
||||
}
|
||||
|
||||
async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||
private async Task<ErrorOr<Success>> PublishCreatePlayListEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
|
||||
@@ -85,8 +85,6 @@ namespace Musify.Application.PlayLists
|
||||
finalPictureKey = playListConfiguration.Routes.BuildOriginalPicturePath(request.UserId, pictureIntent.ObjectName);
|
||||
}
|
||||
|
||||
database.PlayLists.Update(playListEntity);
|
||||
|
||||
if (pictureIntent != null && finalPictureKey != null)
|
||||
{
|
||||
var publishResult = await PublishUpdatePlayListPictureSourceEventAsync(
|
||||
@@ -117,7 +115,7 @@ namespace Musify.Application.PlayLists
|
||||
return PlayListApplicationResponse.FromEntity(playListEntity, coverTrackIds);
|
||||
}
|
||||
|
||||
async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||
private async Task<ErrorOr<Success>> PublishUpdatePlayListPictureSourceEventAsync(
|
||||
Guid playListId,
|
||||
UploadIntent pictureIntent,
|
||||
string finalPictureKey,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -14,6 +14,7 @@ namespace Musify.Application.Tracks
|
||||
public async ValueTask<ErrorOr<TrackApplicationResponse>> Handle(GetLastTrackListenedByUserIdQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var track = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Track.Owner)
|
||||
.Include(t => t.Track.Tags)
|
||||
.Where(t => t.UserId == query.UserId)
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Application.Tracks
|
||||
public async ValueTask<IEnumerable<TrackApplicationResponse>> Handle(GetListeningHistoryQuery query, CancellationToken cancellationToken)
|
||||
{
|
||||
var recentTrackIds = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == query.UserId)
|
||||
.GroupBy(l => l.TrackId)
|
||||
.OrderByDescending(g => g.Max(l => l.ListenedAt))
|
||||
@@ -24,6 +25,7 @@ namespace Musify.Application.Tracks
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var tracksById = await database.Tracks
|
||||
.AsNoTracking()
|
||||
.Include(t => t.Owner)
|
||||
.Include(t => t.Tags)
|
||||
.Where(t => recentTrackIds.Contains(t.Id))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -16,6 +16,7 @@ namespace Musify.Application.Users
|
||||
var weekAgo = DateTime.UtcNow.AddDays(-7);
|
||||
|
||||
var thisWeekHistory = database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo);
|
||||
|
||||
var tracksThisWeek = await thisWeekHistory
|
||||
@@ -24,10 +25,11 @@ namespace Musify.Application.Users
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
var secondsThisWeek = await thisWeekHistory
|
||||
.Join(database.Tracks, l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
|
||||
.Join(database.Tracks.AsNoTracking(), l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds)
|
||||
.SumAsync(cancellationToken);
|
||||
|
||||
var listenedDates = await database.ListeningHistories
|
||||
.AsNoTracking()
|
||||
.Where(l => l.UserId == request.UserId)
|
||||
.Select(l => l.ListenedAt.Date)
|
||||
.Distinct()
|
||||
|
||||
@@ -16,22 +16,22 @@ namespace Musify.Application.Users
|
||||
{
|
||||
public async ValueTask<ErrorOr<PaginatedResponse<UserApplicationResponse>>> Handle(GetUsersQuery request, CancellationToken cancellationToken)
|
||||
{
|
||||
var user = database.Users
|
||||
var usersQuery = database.Users
|
||||
.AsNoTracking()
|
||||
.AsQueryable();
|
||||
|
||||
if (!string.IsNullOrEmpty(request.UsernameSearch))
|
||||
{
|
||||
var normalizedUsername = request.UsernameSearch.Trim().ToUpperInvariant();
|
||||
user = user.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||
usersQuery = usersQuery.Where(u => u.NormalizedName.Contains(normalizedUsername));
|
||||
}
|
||||
|
||||
var totalCount = await user.CountAsync(cancellationToken);
|
||||
var pagedUser = await user
|
||||
var totalCount = await usersQuery.CountAsync(cancellationToken);
|
||||
var pagedUsers = await usersQuery
|
||||
.Select(u => UserApplicationResponse.FromEntity(u))
|
||||
.ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken);
|
||||
|
||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUser);
|
||||
return PaginatedResponse<UserApplicationResponse>.FromPagedList(pagedUsers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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>(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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>(
|
||||
|
||||
backend/Core/Musify.Infrastructure/Persistence/Migrations/20260720180629_WidenTrackTitle.Designer.cs
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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>(
|
||||
|
||||
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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>(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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>(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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)
|
||||
{
|
||||
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+2
-2
@@ -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
|
||||
|
||||
+3
-13
@@ -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(
|
||||
|
||||
Generated
+2
-2
@@ -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
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
-1164
File diff suppressed because it is too large
Load Diff
-106
@@ -1,106 +0,0 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class MakePlayListPictureColumnsOptional : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SmallPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "OriginalPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MediumPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LargePictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "SmallPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "OriginalPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "MediumPictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "LargePictureName",
|
||||
table: "PlayLists",
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(64)",
|
||||
oldMaxLength: 64,
|
||||
oldNullable: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
+31
-27
@@ -1,4 +1,4 @@
|
||||
// <auto-generated />
|
||||
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
@@ -18,7 +18,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.8")
|
||||
.HasAnnotation("ProductVersion", "10.0.12")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "ambient", "blues", "classical", "country", "drum_and_bass", "dubstep", "electronic", "folk", "funk", "hip_hop", "house", "indie", "jazz", "k_pop", "latin", "lofi", "metal", "pop", "punk", "reggae", "reggaeton", "rn_b", "rock", "soul", "techno", "trance" });
|
||||
@@ -68,7 +68,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("Delivered");
|
||||
|
||||
b.ToTable("InboxState");
|
||||
b.ToTable("InboxState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||
@@ -159,7 +159,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("OutboxMessage");
|
||||
b.ToTable("OutboxMessage", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b =>
|
||||
@@ -189,7 +189,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("Created");
|
||||
|
||||
b.ToTable("OutboxState");
|
||||
b.ToTable("OutboxState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Album", b =>
|
||||
@@ -231,7 +231,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
b.ToTable("Albums", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b =>
|
||||
@@ -259,7 +259,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("AlbumId", "TrackId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AlbumHasTrack");
|
||||
b.ToTable("AlbumHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b =>
|
||||
@@ -283,7 +283,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("ListeningHistory");
|
||||
b.ToTable("ListeningHistory", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Mix", b =>
|
||||
@@ -317,7 +317,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId", "Position");
|
||||
|
||||
b.ToTable("Mixes");
|
||||
b.ToTable("Mixes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.MixItem", b =>
|
||||
@@ -341,7 +341,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("MixId", "Position");
|
||||
|
||||
b.ToTable("MixItems");
|
||||
b.ToTable("MixItems", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.PlayList", b =>
|
||||
@@ -385,7 +385,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("PlayLists");
|
||||
b.ToTable("PlayLists", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b =>
|
||||
@@ -412,7 +412,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("TrackId");
|
||||
|
||||
b.ToTable("PlayListHasTrack");
|
||||
b.ToTable("PlayListHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Track", b =>
|
||||
@@ -450,7 +450,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("OwnerUserId");
|
||||
|
||||
b.ToTable("Tracks");
|
||||
b.ToTable("Tracks", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b =>
|
||||
@@ -475,7 +475,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("UserId", "TrackId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TrackLikes");
|
||||
b.ToTable("TrackLikes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.TrackTag", b =>
|
||||
@@ -495,7 +495,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("TrackId", "Tag")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TrackTags");
|
||||
b.ToTable("TrackTags", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.Upload", b =>
|
||||
@@ -524,7 +524,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Upload");
|
||||
b.ToTable("Upload", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b =>
|
||||
@@ -575,7 +575,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UploadIntents");
|
||||
b.ToTable("UploadIntents", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.User", b =>
|
||||
@@ -613,7 +613,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("User");
|
||||
b.ToTable("User", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b =>
|
||||
@@ -638,7 +638,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
b.HasIndex("FollowerId", "FollowedId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("UserFollows");
|
||||
b.ToTable("UserFollows", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b =>
|
||||
@@ -659,7 +659,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("UserHasTrack");
|
||||
b.ToTable("UserHasTrack", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b =>
|
||||
@@ -686,7 +686,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("AlbumProcessingState");
|
||||
b.ToTable("AlbumProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b =>
|
||||
@@ -713,7 +713,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("PlayListProcessingState");
|
||||
b.ToTable("PlayListProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b =>
|
||||
@@ -749,7 +749,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b.HasKey("CorrelationId");
|
||||
|
||||
b.ToTable("TrackProcessingState");
|
||||
b.ToTable("TrackProcessingState", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b =>
|
||||
@@ -803,7 +803,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("AlbumId");
|
||||
|
||||
b1.ToTable("Albums");
|
||||
b1.ToTable("Albums", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("AlbumId");
|
||||
@@ -896,28 +896,32 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("LargeName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("LargePictureName");
|
||||
|
||||
b1.Property<string>("MediumName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("MediumPictureName");
|
||||
|
||||
b1.Property<string>("OriginalName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("OriginalPictureName");
|
||||
|
||||
b1.Property<string>("SmallName")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)")
|
||||
.HasColumnName("SmallPictureName");
|
||||
|
||||
b1.HasKey("PlayListId");
|
||||
|
||||
b1.ToTable("PlayLists");
|
||||
b1.ToTable("PlayLists", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("PlayListId");
|
||||
@@ -988,7 +992,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("TrackId");
|
||||
|
||||
b1.ToTable("Tracks");
|
||||
b1.ToTable("Tracks", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TrackId");
|
||||
@@ -1025,7 +1029,7 @@ namespace Musify.Infrastructure.Persistence.Migrations
|
||||
|
||||
b1.HasKey("TrackId");
|
||||
|
||||
b1.ToTable("Tracks");
|
||||
b1.ToTable("Tracks", (string)null);
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("TrackId");
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -107,5 +107,30 @@ namespace Musify.Application.Tests.PlayLists
|
||||
Assert.Equal("Original Name", result.Value.Name);
|
||||
Assert.Equal("Original description", result.Value.Description);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_PlayListWithoutPictures_ValidPictureIntent_SetsPictureAndPublishesSourceEvent()
|
||||
{
|
||||
storageService
|
||||
.HeadObjectAsync(Arg.Any<string>(), Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new ObjectMetaData("image/webp", 1024));
|
||||
|
||||
var owner = TestEntities.User();
|
||||
var playList = TestEntities.PlayList(owner.Id);
|
||||
playList.Pictures = null;
|
||||
var intent = TestEntities.UploadIntent(owner.Id, Musify.Domain.ValueObjects.UploadIntentPurpose.PlayListPicture, objectName: "new-cover.webp");
|
||||
await SeedAsync(owner, playList, intent);
|
||||
|
||||
var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, intent.Id);
|
||||
|
||||
var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken);
|
||||
|
||||
Assert.False(result.IsError);
|
||||
Assert.Equal("new-cover.webp", result.Value.SmallImageKeyName);
|
||||
var stored = await Database.PlayLists.FindAsync([playList.Id], TestContext.Current.CancellationToken);
|
||||
Assert.NotNull(stored);
|
||||
Assert.Equal("new-cover.webp", stored.Pictures?.OriginalName);
|
||||
await eventBus.Received(1).PublishAsync(Arg.Any<Musify.Application.Events.UpdatePlayListPictureSourceEvent>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -15,7 +15,8 @@ npm run dev # servidor de desarrollo (puerto 5173)
|
||||
npm run build
|
||||
npm run check # svelte-check — debe salir 0 errores antes de cerrar un cambio
|
||||
npm run format # prettier --write .
|
||||
npm run lint # prettier --check . && eslint .
|
||||
npm run lint # prettier --check . && eslint . && npm run lint:tokens
|
||||
npm run lint:tokens # scripts/check-tokens.mjs — guardrail del sistema de diseño (ver abajo)
|
||||
```
|
||||
|
||||
## Reglas
|
||||
@@ -28,6 +29,19 @@ npm run lint # prettier --check . && eslint .
|
||||
- Enlaces a endpoints `+server.ts` (login/logout) llevan `data-sveltekit-reload`.
|
||||
- Reutiliza los tokens/recetas del sistema de diseño; extrae a `$lib/components/ui/` lo que se repita.
|
||||
|
||||
## Estilos
|
||||
|
||||
El sistema de diseño vive en `src/routes/layout.css` (tokens `@theme` + recetas `@utility`) y `src/lib/theme/` (`theme.css` para neutros estáticos, `tokens.ts` para los tokens dinámicos por hue de usuario). `scripts/check-tokens.mjs` (`npm run lint:tokens`, parte de `lint`) falla si `src/**/*.svelte` reintroduce lo que el refactor de `docs/frontend-refactor-plan.md` (P0–P14) eliminó — léelo antes de añadir un valor arbitrario o un radio/tamaño "de fábrica".
|
||||
|
||||
- **Radios:** siempre `rounded-tag/thumb/control/art/art-lg/panel/panel-lg` (o `rounded-full` para píldoras/círculos). Nunca `rounded-sm/md/lg/xl/2xl/3xl` ni `rounded` a secas.
|
||||
- **Tamaños cuadrados:** `size-*` en vez de `h-N w-N` repetido (icon: `size-icon-xs/sm/md/lg/xl`; portadas: `size-cover-xs…hero`).
|
||||
- **Tipografía:** recetas `text-display-1/2/3/4`, `text-eyebrow`, `text-body`, `text-count`; tracking con `tracking-display`/`tracking-eyebrow`, no `tracking-[…]` suelto.
|
||||
- **Espaciado:** escala de Tailwind en pasos de `.5` (evita cuartos como `.25`/`.75`; solo aceptables si el token semántico lo pide, p. ej. `mb-4.5` en `SectionHeading`).
|
||||
- **Colores:** siempre `--mf-*`/`text-*`/`bg-*`/`border-*` de `theme.css`/`tokens.ts`. Nunca hex ni `rgba()` literales en un `.svelte` (esos literales solo existen como fuente de verdad dentro de `theme.css`/`tokens.ts`).
|
||||
- **`style="…"` y arbitrarios `-[…]`:** solo la lista blanca de B.4 en `docs/frontend-refactor-plan.md` (stagger `--i`, hue de tile, posición de menú, anchos/alturas dinámicos vía `var(--mf-*)`, `text-[clamp(…)]`, `grid-cols-[auto_1fr]`, `max-h-[85dvh]`, paddings en `vh` para centrado vertical). Todo lo demás, a un token.
|
||||
- Componentes base a reutilizar antes de crear uno nuevo: `Artwork`, `Avatar`, `ListRow`, `MediaIdentity`, `TrackList`, `MediaCard`, `PageHeader`, `ContextMenu`, `ConfirmDialog`, `PlayButton`, `Slider`, `Logo`, `Chip`, `SegmentedControl`, `CoverForm`.
|
||||
- Gaps conocidos y aceptados (documentados en `scripts/check-tokens.mjs`): `strokeWidth={n}` en iconos no está unificado (13 valores distintos repartidos por casi todos los componentes; migrarlo a las recetas `stroke-thin/regular/bold` de forma segura requeriría poder verificar visualmente que `lucide-svelte` respeta `stroke-width` por CSS, así que se dejó fuera de esta pasada).
|
||||
|
||||
## Auth
|
||||
|
||||
`hooks.server.ts` valida la cookie de sesión cifrada (`mf_session`), refresca el `access_token` y puebla `locals.user` / `locals.accessToken`. Para proteger una ruta, comprueba `locals.user` en su `load` y redirige a `/login?returnTo=…`. Para el backend, envía `locals.accessToken` como Bearer hacia `API_BASE_URL`.
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
# Plan — Unificación del sistema de diseño y limpieza del front (web-player)
|
||||
|
||||
> Este documento es un **plan de trabajo para otro agente/IA**, no un informe de lo ya
|
||||
> hecho. Recoge conocimiento concreto (bugs reales, patrones, cifras) obtenido durante
|
||||
> una sesión larga de ajustes de UI, para que quien continúe no tenga que re-descubrirlo.
|
||||
> No es necesario ejecutar todo de una vez: está pensado para trabajarse página por
|
||||
> página o componente por componente, en PRs pequeños y verificables.
|
||||
|
||||
> **Estado (sesión de seguimiento posterior):** §1–§3 y §5 de este documento (hex
|
||||
> literales, tamaños de fuente arbitrarios, menú "de cristal" duplicado, componentes
|
||||
> huérfanos listados) ya están resueltos. El problema había reaparecido con otra forma:
|
||||
> valores de opacidad sueltos sobre `white`/`black` (`bg-white/[6%]`, `border-white/10`,
|
||||
> etc.) fuera de los tokens `--mf-surface*`/`--mf-border*`, sombras/gradientes de portada
|
||||
> duplicados sin token, un bloque de "cabecera de colección" reimplementado 5 veces, un
|
||||
> menú de cuenta duplicado en `TopBar`/`MobileHeader`, y textos de estado vacío con
|
||||
> redacción inconsistente ("Aún no..." vs "Todavía no..."). Todo eso se corrigió: se
|
||||
> añadieron tokens `--shadow-cover-lg/md/sm/xs` y `--mf-liked-grad` a
|
||||
> `layout.css`/`theme.css`, se migraron los literales `white`/`black` a
|
||||
> `bg-surface*`/`border-line*`/`hover:bg-hover`/`text-on-art` según si el color está
|
||||
> sobre el "chrome" de la app o sobre arte/gradiente saturado, se extrajeron
|
||||
> `lib/components/ui/CollectionHeader.svelte` (portada + título + meta + acciones, usado
|
||||
> en `AlbumHeader`, `PlaylistHeader`, `liked/+page.svelte`, `mixes/[id]/+page.svelte`,
|
||||
> `albums/external/.../+page.svelte` y `u/[id]/+page.svelte`) y
|
||||
> `lib/components/ui/AccountMenu.svelte` (estado de apertura + panel del menú de cuenta,
|
||||
> usado en `TopBar`/`MobileHeader`), y se corrigió `--color-hover` en `layout.css`, que
|
||||
> estaba hardcodeado en vez de apuntar a `--mf-surface-2` (rompía el modo claro para
|
||||
> cualquier `hover:bg-hover`).
|
||||
>
|
||||
> **Repaso posterior a la verificación visual** (ya con sesión autenticada): se detectó
|
||||
> que el fix de §2 anterior para la tarjeta de género de `explore/+page.svelte` estaba
|
||||
> mal — usaba `var(--mf-ink)` como extremo oscuro del degradado, pero `--mf-ink` es el
|
||||
> token de "texto sobre botón de acento" y se invierte a blanco en modo claro, rompiendo
|
||||
> la tarjeta (fondo blanco en vez de degradado oscuro). Se creó un token nuevo,
|
||||
> `--mf-tile-shade` (deliberadamente **sin** variante de modo claro, igual que
|
||||
> `--mf-cover-grad`/`--mf-liked-grad`, porque es "arte" decorativo, no "chrome"), y se usa
|
||||
> ahí en su lugar. También se cambió la línea de meta de `CollectionHeader` de
|
||||
> `text-muted` a `text-fg-2` (mejor contraste en modo claro; `text-muted` rondaba ~3:1,
|
||||
> por debajo de WCAG AA para texto normal) y se hizo `actions` opcional para poder migrar
|
||||
> páginas sin acciones condicionales (como `u/[id]/+page.svelte`) sin renderizar un
|
||||
> `<div>` vacío.
|
||||
>
|
||||
> Pendiente de quien retome esto: `lib/components/ui/Rail.svelte` no tiene importadores
|
||||
> (confirmado con el mismo método de §5, no porque §5 lo listara — ese hallazgo es nuevo)
|
||||
> — no se tocó porque no forma parte de este barrido, pero conviene confirmarlo a mano y
|
||||
> borrarlo si procede. También queda pendiente repetir la verificación visual (§6) sobre
|
||||
> los cambios de este repaso: tarjetas de género de Explorar, cabeceras de colección
|
||||
> (contraste de la línea de meta) y `u/[id]/+page.svelte`.
|
||||
|
||||
## 0. Objetivo
|
||||
|
||||
Tres problemas, en orden de aparición histórica pero **no** de prioridad recomendada:
|
||||
|
||||
1. **Colores fijos en vez de variables**: muchas páginas y componentes escriben colores
|
||||
literales (`text-[#8a8a8e]`, `bg-[#c6c4cb]`, tamaños `text-[13px]` sueltos, etc.) en
|
||||
vez de usar los tokens del sistema (`text-fg-2`, `bg-surface`, la escala tipográfica
|
||||
establecida). Esto es lo que impide que cosas como el modo claro/oscuro (ver §2)
|
||||
funcionen de forma consistente, y hace que cada página tenga ligeras variaciones de
|
||||
gris que deberían ser el mismo tono.
|
||||
2. **Falta de componentización**: hay patrones (tarjeta de track, fila de "canción
|
||||
reciente", menú desplegable tipo cristal, etc.) reimplementados a mano en varias
|
||||
páginas en vez de reusar (o crear) un componente compartido. Algunos componentes ya
|
||||
existentes ni siquiera se usan donde deberían.
|
||||
3. **Código muerto**: imports sin usar, componentes sin importadores, clases residuales.
|
||||
|
||||
**Referencia canónica**: la página de Inicio (`src/routes/+page.svelte`) es el patrón a
|
||||
seguir para el modo oscuro — sus tonos, jerarquía tipográfica y look general están bien
|
||||
ajustados (fue la página más iterada). Cuando una página no tenga claro qué tono/tamaño
|
||||
usar para algo, mirar cómo lo resuelve Inicio para lo mismo, **no inventar uno nuevo**.
|
||||
Ojo: Inicio también usa hex sueltos en vez de tokens en varios sitios (ver §3) — es la
|
||||
referencia de **valores/jerarquía visual**, no de "cómo escribir la clase". Cuando se
|
||||
migre Inicio a tokens, los valores resultantes deben ser los mismos que ya tiene hoy.
|
||||
|
||||
---
|
||||
|
||||
## 1. Limitación conocida: modo claro ("Modo blanco") incompleto
|
||||
|
||||
Se añadió un toggle de tema (`$lib/theme/mode.svelte.ts`, menú de cuenta en
|
||||
`TopBar.svelte` / `MobileHeader.svelte`) que pone `data-theme="light"` en `<html>` y
|
||||
persiste la preferencia en `localStorage` (`musify.theme`).
|
||||
|
||||
**Lo que funciona:** los tokens semánticos definidos como CSS puro en
|
||||
`$lib/theme/theme.css` (`--mf-text*`, `--mf-surface*`, `--mf-border*`, `--mf-track`,
|
||||
`--mf-scrim`, `--mf-ink`) tienen su contrapartida en el bloque
|
||||
`:root[data-theme='light'] { ... }` al final de ese archivo, y los tokens dinámicos por
|
||||
hue (`--mf-bg`, `--mf-elevated`, `--mf-sidebar-bg`, `--mf-panel-bg`, `--mf-bar-bg`,
|
||||
`--mf-hairline`) se regeneran correctamente en modo claro dentro de
|
||||
`buildThemeTokens(hue, mode)` (`$lib/theme/tokens.ts`).
|
||||
|
||||
**Gotcha importante para quien toque esto:** `+layout.svelte` reaplica esos tokens
|
||||
dinámicos como **estilos inline** sobre `document.documentElement` en un `$effect` que
|
||||
corre en cada cambio de hue (cada canción). Un estilo inline siempre gana sobre
|
||||
cualquier regla de una hoja de estilos, así que **una variable que se anima por hue no
|
||||
se puede sobrescribir con `:root[data-theme='light'] { --mf-x: ... }`** — hay que
|
||||
añadirla dentro de `buildThemeTokens()` con una rama `light ? ... : ...`, no en
|
||||
`theme.css`. Si en el futuro se anima algo más por hue, aplica la misma regla.
|
||||
|
||||
**Lo que NO funciona:** todo componente que usa un color hardcodeado (ver §3) no se
|
||||
entera del cambio de tema, porque no está leyendo ningún token. En la práctica esto
|
||||
significa que hoy el modo claro cambia correctamente el lienzo de fondo, la sidebar, los
|
||||
paneles tipo cristal (menús, modal) y cualquier texto/superficie que ya use
|
||||
`text-fg`/`bg-surface`/etc., pero deja con contraste pobre o directamente ilegible
|
||||
cualquier texto en hex literal (la mayoría de títulos, subtítulos y metadatos de la
|
||||
app hoy).
|
||||
|
||||
**No hay forma barata de arreglar esto sin la migración de §3.** El modo claro real
|
||||
depende, técnicamente, en un 100%, de terminar esa migración. No merece la pena
|
||||
"parchear" el modo claro sin más — es la misma tarea.
|
||||
|
||||
---
|
||||
|
||||
## 2. Inventario de bugs/patrones ya encontrados (usar como guía de búsqueda)
|
||||
|
||||
Estos son bugs concretos que se repitieron varias veces en distintos componentes esta
|
||||
sesión. Antes de tocar cada página, buscar estos mismos patrones ahí:
|
||||
|
||||
### 2.1 Tamaño de fuente que falta → hereda 16px del navegador
|
||||
|
||||
Varios componentes tenían un texto sin ninguna clase `text-*`, lo que hace que use el
|
||||
tamaño por defecto del navegador (16px) en vez de la escala real de la app (que va de
|
||||
`10.5px` a `14px` en casi todo). Ya se corrigió en `MediaCard.svelte`,
|
||||
`TrackTitleCell.svelte` y `MenuItem.svelte`. **Sigue sin corregir** en
|
||||
`TrackContextMenu.svelte` (usa `text-base`/`text-sm` sueltos igual que tenía
|
||||
`MenuItem` antes de arreglarlo — mismo fix: bajar a la escala `13–13.5px` y los iconos
|
||||
de `h-6 w-6`/`h-5 w-5` a `h-[18px] w-[18px]` o similar).
|
||||
|
||||
Cómo detectarlo: cualquier `<div>`/`<span>` que pinte texto de usuario (título, nombre,
|
||||
label) sin una clase `text-[…]`/`text-sm`/`text-xs` explícita es sospechoso.
|
||||
|
||||
### 2.2 Conflicto de clases Tailwind que se resuelve por orden interno, no por el HTML
|
||||
|
||||
Tailwind no respeta el orden en que escribes las clases en el `class="…"`; resuelve
|
||||
conflictos de la misma propiedad (`background-color`, `font-weight`, etc.) por el orden
|
||||
en que esas utilidades existen en su propia hoja de estilos generada. Esto causó bugs
|
||||
reales y confusos:
|
||||
|
||||
- Un botón con `bg-white/[6%] ... {isCurrent ? 'bg-accent-tint' : ''}` — con `isCurrent`
|
||||
true, el fondo **seguía viéndose** `bg-white/[6%]` porque esa clase "ganaba" en el
|
||||
cascade aunque apareciera antes en el HTML.
|
||||
- `Button.svelte` fuerza `font-semibold` en su clase base; para poner un botón en peso
|
||||
normal (`variant="secondary"` con texto normal) hay que usar `class="!font-normal"`
|
||||
(con el prefijo `!` de Tailwind, que sí fuerza especificidad), un `class="font-normal"`
|
||||
a secas no habría ganado.
|
||||
|
||||
**Regla a aplicar en la auditoría**: cualquier sitio donde dos clases de la misma
|
||||
propiedad CSS convivan condicional + incondicionalmente en el mismo elemento (patrón
|
||||
`class="base-x ... {cond ? 'other-x' : ''}"`) hay que reescribirlo para que sea
|
||||
mutuamente excluyente (`class="{cond ? 'a' : 'b'}"`), nunca acumulativo. Y cualquier
|
||||
override de una clase base de un componente compartido (`Button`, etc.) debe usar `!`
|
||||
si toca la misma propiedad que ya fija la clase base.
|
||||
|
||||
### 2.3 Iconos con brillo verde/acento en `:hover` de forma inconsistente
|
||||
|
||||
Varias tarjetas tenían `group-hover:text-accent-soft` en el título para dar un hover
|
||||
"bonito", pero era inconsistente con el resto de la app (la mayoría de textos no
|
||||
cambian de color al hover, solo cuando están realmente "activos"/reproduciéndose). Se
|
||||
quitó de `MediaCard.svelte` y de la fila de "Tus playlists" en Inicio. **Quedan
|
||||
pendientes** los mismos `group-hover/card:text-accent-soft` /
|
||||
`group-hover/row:text-accent-soft` en `src/routes/(app)/explore/+page.svelte` (líneas
|
||||
con tarjetas de álbum y filas de resultado — grep por `group-hover.*text-accent-soft`
|
||||
para localizarlos exactos, las líneas se mueven con cada edición).
|
||||
|
||||
---
|
||||
|
||||
## 3. Migración de colores/tamaños a tokens
|
||||
|
||||
### 3.1 Datos de partida (medidos en esta sesión, `src/` de web-player)
|
||||
|
||||
```
|
||||
27 archivos .svelte con al menos un text-[#…]/bg-[#…]/border-[#…]
|
||||
106 apariciones totales de color hex literal en clases Tailwind
|
||||
```
|
||||
|
||||
Los 10 valores hex más repetidos como `text-[#…]` (comando usado:
|
||||
`grep -roE "(text|bg|border)-\[#[0-9a-fA-F]{3,8}\]" --include="*.svelte" . | sort | uniq -c | sort -rn`):
|
||||
|
||||
| Ocurrencias | Valor | Coincide EXACTO con token |
|
||||
| ----------- | --------- | ------------------------------------- |
|
||||
| 13 | `#8a8a8e` | `--mf-text-2` → usar `text-fg-2` |
|
||||
| 9 | `#747478` | ninguno exacto (cerca de text-3/4) |
|
||||
| 7 | `#d5d4d8` | ninguno exacto (cerca de `--mf-text`) |
|
||||
| 7 | `#7f7f83` | ninguno exacto (cerca de text-3) |
|
||||
| 6 | `#d9d8dc` | `--mf-text` → usar `text-fg` |
|
||||
| 6 | `#d4d3d7` | ninguno exacto |
|
||||
| 4 | `#d0cfd4` | ninguno exacto |
|
||||
| 4 | `#818185` | ninguno exacto |
|
||||
| 3 | `#d8d7db` | ninguno exacto |
|
||||
| 2 | `#7d7d81` | `--mf-text-3` → usar `text-fg-3` |
|
||||
|
||||
Interpretación: hay un grupo de valores que son **copias exactas** de un token ya
|
||||
existente (probablemente porque alguien copió el valor en vez de la clase) — esos son
|
||||
reemplazo mecánico, bajo riesgo. Y hay un grupo mucho más grande de valores "parecidos
|
||||
pero no iguales" a los 4 tonos de texto (`--mf-text`, `-2`, `-3`, `-4`) — esto sugiere
|
||||
que cada componente inventó su propio gris en vez de reusar la escala, y hay que decidir
|
||||
para cada uno **a qué tier semántico pertenece** (¿es un título? ¿un metadato? ¿un
|
||||
label?) y mapearlo al token de esa categoría, no crear un tier nuevo por cada valor.
|
||||
|
||||
Solo se encontraron 3 `bg-[#…]` (en `MixTile.svelte` y `TransportControls.svelte`,
|
||||
este último repite el mismo `#c6c4cb`/`#d6d5da` que tenía el botón "Reanudar" de Inicio
|
||||
antes de moverlo a `Button variant="accent"` con override — es el mismo "botón claro"
|
||||
reinventado dos veces; candidato a token propio, ver §4.4) y 0 `border-[#…]`.
|
||||
|
||||
### 3.2 Metodología sugerida (por archivo)
|
||||
|
||||
1. `grep -n "text-\[#\|bg-\[#\|border-\[#" archivo.svelte` para listar todos los casos.
|
||||
2. Para cada uno, decidir el tier semántico (título/cuerpo/metadato/label/borde) y
|
||||
sustituir por la utilidad de Tailwind ya mapeada en
|
||||
`src/routes/layout.css` (`@theme` → `--color-fg`, `--color-fg-2`, `--color-fg-3`,
|
||||
`--color-muted`, `--color-surface`, `--color-surface-2`, `--color-line`,
|
||||
`--color-accent*`, etc.) o, si el valor es realmente un tamaño de fuente en vez de
|
||||
color, a la escala ya usada en el resto de la app (10.5 / 11 / 11.5 / 12 / 12.5 / 13 /
|
||||
13.5 / 14px — no inventar un tamaño intermedio nuevo salvo que Inicio ya lo use para
|
||||
ese mismo tipo de texto).
|
||||
3. Ejecutar `npm run check` tras cada archivo — Tailwind con clases arbitrarias no da
|
||||
error de compilación aunque el token no exista, así que la única validación real es
|
||||
visual: abrir la página en el navegador (Browser pane) y comparar contra Inicio.
|
||||
4. Si dos o más componentes usan **el mismo valor hex no-token** para el mismo propósito
|
||||
(p. ej. `#c6c4cb` como "botón claro"), considerar promoverlo a un token nuevo en
|
||||
`theme.css` en vez de simplemente sustituirlo por el más parecido — ver §4.4.
|
||||
5. `npm run lint` (prettier + eslint) antes de dar por cerrado cada archivo, según las
|
||||
reglas del `CLAUDE.md` del proyecto.
|
||||
|
||||
### 3.3 Prioridad sugerida de páginas/componentes a migrar
|
||||
|
||||
Por impacto (páginas que ve todo usuario, no solo estados de error/edge):
|
||||
|
||||
1. `Sidebar.svelte`, `TopBar.svelte`, `MobileHeader.svelte` (chrome global, visible
|
||||
siempre) — MobileHeader/TopBar ya tienen el menú de cuenta arreglado, falta el resto.
|
||||
2. `src/routes/(app)/playlists/[id]/components/PlaylistHeader.svelte` y
|
||||
`src/routes/(app)/albums/[id]/components/AlbumHeader.svelte` (comparten patrón,
|
||||
arreglar juntos para no divergir otra vez).
|
||||
3. `src/routes/(app)/liked/+page.svelte`, `src/routes/(app)/explore/+page.svelte`,
|
||||
`src/routes/(app)/upload/+page.svelte`, `src/routes/(app)/mixes/[id]/+page.svelte`.
|
||||
4. Componentes del reproductor (`lib/components/player/*.svelte`) — muchos hex propios,
|
||||
pero cambian menos visualmente entre páginas, así que menor prioridad de contraste
|
||||
pero igual de importantes para el modo claro (la barra del reproductor es visible en
|
||||
toda la app).
|
||||
5. `src/routes/+page.svelte` (Inicio) al final — es la referencia, así que migrarla
|
||||
última minimiza el riesgo de que un "acabado" a medio migrar se convierta en la
|
||||
referencia visual equivocada mientras se trabaja el resto.
|
||||
|
||||
---
|
||||
|
||||
## 4. Componentización pendiente
|
||||
|
||||
### 4.1 Patrón de fila de track reimplementado en vez de reusar `TrackTitleCell`
|
||||
|
||||
`src/routes/+page.svelte` reimplementa a mano, con su propio marcado, **tres** variantes
|
||||
de "fila de canción" distintas entre sí (Continuar escuchando, Populares esta semana,
|
||||
filas de la Playlist destacada) en vez de usar los componentes ya existentes
|
||||
`TrackTitleCell.svelte` / `TrackRow.svelte` / `TrackMeta.svelte` /
|
||||
`TrackIndexCell.svelte` que sí se usan correctamente en
|
||||
`PlaylistTrackTable.svelte`/`library/+page.svelte`. Evaluar si esas tres filas de Inicio
|
||||
se pueden expresar con los componentes de `TrackTable` (aunque sea sin la tabla
|
||||
completa, solo reusando `TrackTitleCell` para title+cover+artist) para no mantener 4
|
||||
implementaciones de "fila de canción" en paralelo.
|
||||
|
||||
### 4.2 `MediaCard`/`PlaylistCard`/`AlbumCard` vs. tarjetas hechas a mano
|
||||
|
||||
`src/routes/+page.svelte` (sección "Tus playlists") y `src/routes/(app)/explore/+page.svelte`
|
||||
tienen su propia tarjeta de álbum/playlist en vez de usar `MediaCard`/`PlaylistCard`/
|
||||
`AlbumCard` (`lib/components/ui/`). Antes de tocar el estilo de esas tarjetas a mano,
|
||||
comprobar si pueden sustituirse directamente por el componente compartido — sale gratis
|
||||
el arreglo de tipografía/color si el componente compartido ya está migrado a tokens.
|
||||
|
||||
### 4.3 Menú desplegable "de cristal" duplicado
|
||||
|
||||
`TopBar.svelte` y `MobileHeader.svelte` duplican: estado `menuOpen`, el
|
||||
`onDocumentClick` para cerrar al hacer click fuera, y el marcado del panel
|
||||
(`animate-pop ... backdrop-blur-[22px]` + `background-color:var(--mf-panel-bg);
|
||||
background-image:var(--mf-modal-glow)`). `TrackContextMenu.svelte` tiene un menú
|
||||
flotante distinto (`bg-elevated` + `border-line`, sin el efecto cristal, y con el bug de
|
||||
tamaño de fuente de §2.1). Candidatos:
|
||||
|
||||
- Extraer un primitivo `Dropdown.svelte` o `FloatingMenu.svelte` que resuelva
|
||||
posicionamiento + cierre al clicar fuera + Escape, y que ambos menús de cuenta lo usen.
|
||||
- Definir la receta "panel de cristal" (el `style="background-color:var(--mf-panel-bg);
|
||||
background-image:var(--mf-modal-glow)"` + `backdrop-blur-[22px]`) en una utilidad o
|
||||
clase reusable en vez de repetir el `style=` inline en cada sitio, y aplicarla también
|
||||
a `TrackContextMenu` para que los tres menús floating de la app se vean iguales.
|
||||
|
||||
### 4.4 Candidato a token nuevo: "botón claro" (`#c6c4cb`/`#d6d5da`/`#d8d7dc`)
|
||||
|
||||
Aparece en al menos: el antiguo botón "Reanudar" de Inicio (ya migrado a
|
||||
`Button variant="accent"` con `class="!bg-white ..."`, ver commit reciente) y
|
||||
`TransportControls.svelte` (botón de play principal del reproductor, sigue con hex
|
||||
propio). Si el diseño quiere mantener un botón "casi blanco" como acento fuerte en
|
||||
ciertos sitios (distinto del verde de acento normal), vale la pena darle un token
|
||||
propio (p. ej. `--mf-cta-strong-bg`/`--mf-cta-strong-fg`) en vez de repetir el hex cada
|
||||
vez, así el modo claro también puede darle un tratamiento coherente en vez de heredar el
|
||||
mismo blanco fijo en ambos temas (que en modo claro quedaría invisible sobre el fondo).
|
||||
|
||||
---
|
||||
|
||||
## 5. Código muerto — puntos de partida
|
||||
|
||||
Ya se han borrado en esta rama `MobileNav.svelte` y `MixBentoTile.svelte` por no
|
||||
usarse. Grep rápido (basado en "ningún otro `.svelte`/`.ts` bajo `src/` menciona el
|
||||
nombre del componente fuera de su propio archivo") encontró estos candidatos
|
||||
adicionales — **verificar a mano antes de borrar** (el grep no será 100% preciso: no
|
||||
detecta uso vía imports dinámicos, alias, o si el componente se monta desde fuera de
|
||||
`src/`):
|
||||
|
||||
- `src/lib/components/ui/CoverBadge.svelte`
|
||||
- `src/lib/components/ui/PlayPauseOverlay.svelte`
|
||||
|
||||
Método para repetir esta búsqueda tras cada ronda de refactor (los nombres de
|
||||
componente que queden sin importadores son limpieza segura):
|
||||
|
||||
```bash
|
||||
for f in $(find src/lib/components -name "*.svelte"); do
|
||||
name=$(basename "$f" .svelte)
|
||||
count=$(grep -rl "$name" --include="*.svelte" --include="*.ts" src | grep -v "/$name.svelte$" | wc -l)
|
||||
[ "$count" -eq 0 ] && echo "$f"
|
||||
done
|
||||
```
|
||||
|
||||
Además de componentes huérfanos, revisar por archivo (esto no se ha auditado
|
||||
sistemáticamente esta sesión, son solo los tipos de código muerto más comunes en este
|
||||
código base):
|
||||
|
||||
- Imports de iconos (`@lucide/svelte/icons/*`) que dejaron de usarse tras quitar un
|
||||
icono de un botón (pasó varias veces esta sesión con `Play`/`Pause`/`NowPlaying`;
|
||||
`eslint` los marca como `no-unused-vars` — correr `npx eslint <archivo>` tras cada
|
||||
cambio de marcado, no solo `svelte-check`, porque `svelte-check` no siempre lo pilla).
|
||||
- Props de componentes que ya no lee nadie (ej. si se elimina el único sitio que pasaba
|
||||
`description` a `PlaylistCard`, y el componente ya no la usa, quitar el prop además
|
||||
del sitio que la usaba).
|
||||
- Clases Tailwind puestas "por si acaso" que no tienen efecto porque una clase
|
||||
posterior en el cascade las anula (ver §2.2) — no es código muerto en el sentido
|
||||
clásico, pero es ruido que confunde a quien lea el archivo después.
|
||||
|
||||
---
|
||||
|
||||
## 6. Cómo verificar cada cambio
|
||||
|
||||
Del `CLAUDE.md` del proyecto (`web-player/CLAUDE.md`), aplica igual aquí:
|
||||
|
||||
```sh
|
||||
npm run check # svelte-check — debe salir 0 errores
|
||||
npm run lint # prettier --check . && eslint . — debe salir limpio
|
||||
```
|
||||
|
||||
Y para lo visual, que `svelte-check`/`eslint` no cubren:
|
||||
|
||||
1. Levantar `musify-web` (y `musify-api` si la página necesita datos reales) con el
|
||||
Browser pane / `.claude/launch.json`.
|
||||
2. Abrir la página tocada y la página de Inicio **una al lado de otra** (o alternando)
|
||||
para comparar tono de gris, tamaño de fuente y separaciones — no solo "que no se vea
|
||||
roto", sino "que se vea igual que Inicio para el mismo tipo de elemento".
|
||||
3. Si la página tiene estado activo/actual (ej. una fila de canción reproduciéndose),
|
||||
probarlo — varios de los bugs de esta sesión (§2.2) solo eran visibles con el
|
||||
`isCurrent`/hover activo, no en el estado por defecto.
|
||||
4. Alternar el toggle de tema (menú de cuenta → "Modo blanco"/"Modo oscuro") en la
|
||||
página tocada para confirmar que el texto sigue siendo legible en ambos modos.
|
||||
|
||||
---
|
||||
|
||||
## 7. Orden de trabajo sugerido
|
||||
|
||||
1. Terminar §2 (los 3 bugs conocidos que quedan sueltos: `TrackContextMenu.svelte`
|
||||
tamaño de fuente, hover verde en `explore/+page.svelte`).
|
||||
2. Migrar a tokens los componentes de chrome global (§3.3, punto 1) — máximo impacto
|
||||
visual con menor superficie de código.
|
||||
3. Extraer el "panel de cristal" y/o `Dropdown.svelte` (§4.3) **antes** de tocar
|
||||
`TrackContextMenu` a mano, para no maquetarlo dos veces.
|
||||
4. Migrar el resto de páginas por la prioridad de §3.3.
|
||||
5. Revisar componentización (§4.1, §4.2) — mejor después de que los componentes base
|
||||
(`TrackTitleCell`, `MediaCard`, etc.) ya estén en tokens, así "adoptarlos" en Inicio
|
||||
no introduce una regresión visual.
|
||||
6. Pasada final de código muerto (§5) con el listado de componentes/props ya
|
||||
consolidado tras los pasos anteriores (habrá más candidatos a huérfanos según se
|
||||
vayan unificando duplicados).
|
||||
7. Migrar Inicio a tokens en último lugar (§3.3, punto 5), verificando que los valores
|
||||
resultantes son pixel-iguales a los que tiene hoy (es la referencia, no debe cambiar
|
||||
de aspecto, solo de cómo está escrito el código).
|
||||
@@ -772,21 +772,21 @@ Tareas:
|
||||
|
||||
| Parte | Estado | Commit | Notas / decisiones tomadas |
|
||||
| ----- | ------ | ------ | -------------------------- |
|
||||
| P0 | ⬜ | | |
|
||||
| P1 | ⬜ | | |
|
||||
| P2 | ⬜ | | |
|
||||
| P3 | ⬜ | | |
|
||||
| P4 | ⬜ | | |
|
||||
| P5 | ⬜ | | |
|
||||
| P6 | ⬜ | | |
|
||||
| P7 | ⬜ | | |
|
||||
| P8 | ⬜ | | |
|
||||
| P9 | ⬜ | | |
|
||||
| P10 | ⬜ | | |
|
||||
| P11 | ⬜ | | |
|
||||
| P12 | ⬜ | | |
|
||||
| P13 | ⬜ | | |
|
||||
| P14 | ⬜ | | |
|
||||
| P0 | ✅ | (pendiente) | Button primary/secondary se mantienen iguales (decisión del usuario) |
|
||||
| P1 | ✅ | (pendiente) | Verificado con `build` (Tailwind compila `@apply sm:` dentro de `@utility`); sin QA visual autenticada (Docker no disponible en esta sesión) |
|
||||
| P2 | ✅ | (pendiente) | `<style>{expr}</style>` no interpola en Svelte (se trata como CSS del componente); solución: `{@html \`<style>${css}</style>\`}` |
|
||||
| P3 | ✅ | (pendiente) | Se quitó el nombre bajo el avatar de perfil (decisión tomada: redundante con el título de CollectionHeader). Se añadió `size="fill"` a Artwork (no está en la spec) para las tarjetas de grid fluidas (AlbumCard/PlaylistCard/MixTile), ya que P9 (MediaCard) aún no existe. SearchResultRow perdió su prop `kind`/radio-por-tipo (ahora el radio lo decide Artwork/Avatar). Sin QA visual autenticada (Docker no disponible) |
|
||||
| P4 | ✅ | (pendiente) | Decisión tomada sin bloquear: se elimina el botón "adelante" decorativo (TopBar) y el icono `AlignJustify` decorativo (PlayerExtras) por ser código muerto visual. `TransportControls` (shuffle/prev/next/repeat) se deja sin migrar a IconButton a propósito: son iconos sin chrome (sin fondo/hit-box), migrarlos habría añadido una píldora de hover no presente hoy — riesgo visual no verificable sin Docker |
|
||||
| P5 | 🟡 | (pendiente) | Hecho: pressable/clickOutside, toggle.svelte.ts, plural() (+minúsculas, Δ visible), helpers de player.svelte.ts (toQueueItem/isQueueCurrent/playAllOrToggle/playShuffled, addToQueue→playNextItem), navigation.svelte.ts (searchHref/appNavLinks — appNavLinks creado pero su migración a Sidebar/TabsBar se deja para P12 tal como indica esa parte), menus.svelte.ts (createTrackMenu, migrado en Inicio/Explorar/Mezcla). **Pendiente a propósito**: la tarea 5 (eliminar el envoltorio `TrackTarget` de tracks.ts y sus 8 accesores) se difiere — es el cambio de mayor riesgo del plan (toca tipos en Inicio/Explorar/Mezcla/menús de forma transversal) y no se pudo verificar visualmente sin Docker; abordarlo junto con P13 cuando se reescriban esas páginas |
|
||||
| P6 | ✅ | (pendiente) | z-index de AccountMenu unificado a z-(--z-menu) (antes z-20, ahora al mismo nivel que ContextMenu — Δ menor, mejora consistencia); sin QA visual autenticada (Docker no disponible) |
|
||||
| P7 | ✅ | (pendiente) | MediaIdentity queda "puro" (sin onClick) según spec; las 6 tablas de pista (P8) envuelven su propio `use:pressable` hasta que P8 las sustituya por TrackList. Deltas documentados: fila activa de Sidebar ahora usa bg-accent-tint/text-accent-soft (antes gris neutro, ahora consistente con el resto de filas activas); tarjeta "Tus playlists" de Inicio pierde la línea "por {tu nombre}" (redundante, siempre el propio usuario) y su radio pasa de rounded-panel a rounded-art (consistencia con la tarjeta "Continuar"); filas de Queue "A continuación" pierden el opacity-80 del arte (ListRow no expone esa personalización). Sin QA visual autenticada (Docker no disponible) |
|
||||
| P8 | ✅ | (pendiente) | Decisión tomada (recomendada por el propio plan, sin bloquear): se elimina la columna "Álbum" que siempre mostraba "—". TrackList unifica TrackTable+TrackRow+TrackIndexCell+TrackMeta; AlbumTrackTable/PlaylistTrackTable/LibraryPicker se inlinean en sus páginas y se borran. Sin QA visual autenticada (Docker no disponible) |
|
||||
| P9 | ✅ | (pendiente) | MediaCard gana hover-zoom del arte y scrim+PlayButton condicionados a `onPlay` (antes exclusivos de MixTile); ahora se aplicarían a cualquier card futura con onPlay, consistencia intencional. Sin QA visual autenticada (Docker no disponible) |
|
||||
| P10 | ✅ | (pendiente) | Decisión tomada (recomendada, sin bloquear): iconos en los botones de cabecera de playlist (antes solo álbum tenía). PlaylistHeader pierde su prop `id` sin usar tras inlinearlo. Explorar: la cabecera de "Resultados para…" ahora usa el `mb-7/8` de PageHeader en vez de un `mt-5` ajustado a mano antes de los chips de filtro — gap ligeramente mayor entre el resumen y los chips (Δ menor, documentado). Sin QA visual autenticada (Docker no disponible) |
|
||||
| P11 | ✅ | (pendiente) | CoverForm añade snippet `extra` (no está en la spec literal, solo `fields`) para el contenido a ancho completo bajo la fila portada+campos (descripción de álbum, selector de visibilidad de playlist) que no cabía en una única columna. Upload usa ImageDropzone con `onselect` (prop nueva) y se envuelve en `{#key}` para poder resetear tras publicar — Δ: el contenedor de portada pasa de 144/176px (móvil/desktop) a 144px fijo (size-cover-hero, sin variante desktop mayor), y el icono de posición vacía de 64px a 36px (icon-xl), tal como recomienda el plan. Sin QA visual autenticada (Docker no disponible) |
|
||||
| P12 | ✅ | (pendiente) | PlayerExtras se fusiona en PlayerBar (volumen/cola inline); Slider sustituye a SeekBar+VolumeControl (confirmado por grep que el modo `compact` de SeekBar era código muerto). Sidebar y TabsBar migran a `appNavLinks()` (P5). Decisión tomada (sin bloquear): Sidebar gana la marca-gradiente `Logo` que no tenía antes, para consistencia con MobileHeader/login. z-index de PlayerDock/MobileHeader corregido a `z-(--z-sticky)` (son barras fijas, no menús) y SegmentedControl a `z-(--z-raised)`, tal como indica B.1.5. `glass-bar`/`theme-transition` aplicados en PlayerBar (ambas variantes), TabsBar, Queue e Inicio (hero/spotlight), sustituyendo estilos duplicados. Sin QA visual autenticada (Docker no disponible); smoke test de `/login` sin errores de consola |
|
||||
| P13 | ✅ | (pendiente) | Se aborda junto con esta parte la tarea 5 diferida en P5: se elimina el envoltorio `TrackTarget` de `tracks.ts` y sus 8 accesores (`targetId`/`targetTitle`/`targetArtist`/`targetOwnerUserId`/`targetExplicit`/`targetListensCount`/`isTargetCurrent`/`queueItemForTarget`/`targetForQueueItem`); Explorar, Inicio, Mezcla, `menus.svelte.ts` y `mixes.ts` pasan a usar los objetos de pista (`ApiTrackLike`/`LocalTrack`) directamente, con `toQueueItem`/`trackFromQueueItem` (nuevo, en `player.svelte.ts`) como únicos conversores. TypeScript reveló que el campo `isExplicit` no existe en el schema real de `/tracks` — el badge "explicit" de Explorar ya era un no-op antes del refactor (siempre `undefined ?? false`); se retira el binding en vez de fingir soporte. Explorar: `GENRE_INFO`/`genreTiles` → `src/lib/genres.ts`; lógica de búsqueda (`showGroup`/`capped`/`matchPlaylists`/`searchCounts`/`searchChips`/`findTopResult`) → `src/lib/search.ts` (funciones puras, parametrizadas por `sfilter`/`query` en vez de cerrar sobre el estado del componente); "Mejor resultado" unifica el botón/enlace duplicado con `<svelte:element this={...}>`. Decisión tomada sin bloquear: **no** se colapsan los 4 bloques de grupo (Canciones/Álbumes/Usuarios/Playlists) en un único `{#each groups}` — cada grupo tiene snippets, contextmenu y props de `ListRow` genuinamente distintos (menú de pista vs. de álbum, overlay solo en canciones, trailing con formato distinto), y forzar una configuración genérica habría añadido más indirección de la que elimina. Inicio: usa `Page` (antes `page-x pt-3 sm:pt-4` a mano); stats ×3 → `{#each}`; fondos de hero/spotlight (`style=` con `var(--mf-hero-bg)`/`var(--mf-spotlight-bg)`) → utilities `hero-surface`/`spotlight-surface` (nuevas, en `layout.css`) y token `--spacing-section: 2.875rem` → `gap-section` sustituye `gap-11.5`. Sin QA visual autenticada (Docker no disponible); `/explore` sin sesión redirige a `/login` porque `+layout.server.ts` exige `allowAnonymousListening` desde el backend (inalcanzable sin Docker) — comportamiento preexistente, no una regresión; smoke test de `/login` sin errores de consola |
|
||||
| P14 | ✅ | (pendiente) | `scripts/check-tokens.mjs` (`npm run lint:tokens`, añadido a `lint`) escanea `src/**/*.svelte` y falla en: arbitrarios `-[…]` fuera de la lista blanca de B.4, `rounded-(sm/md/lg/xl/2xl/3xl)`/`rounded` a secas, pares `h-N w-N` (deberían ser `size-N`), espaciado fraccionario en cuartos (`.25`/`.75`), hex/`rgba()` literales. Se limpió el repo hasta dejarlo en 0 violaciones: ~40 pares `h-N w-N` → `size-N` (icons sobre todo, vía script puntual), ~26 espaciados en cuartos redondeados al entero más cercano (Δ≤1px, p. ej. `py-2.75`→`py-3`, `gap-1.75`→`gap-2`), 3 `rounded-sm/lg` + 1 `rounded` a secas → tokens (`rounded-tag`/`rounded-thumb`/`rounded-control`), 2 `tracking-[-0.02em]` → `tracking-display` (coincidían exactamente con el token ya existente y no se habían migrado), 1 `tracking-[0.14em]` en Explorar → receta `text-eyebrow`, `rounded-3xl` del hero de Inicio → `rounded-panel-lg` (consistente con la tarjeta de playlist destacada). Se retiraron las recetas `stroke-thin/regular/bold` (creadas en P1, 0 consumidores) — **decisión tomada, sin bloquear**: no se migra `strokeWidth={n}` (13 valores, en casi todos los componentes) a esas recetas por CSS porque no se puede verificar sin Docker que `lucide-svelte` respete `stroke-width` por clase en vez del prop, y una regresión ahí sería silenciosa en toda la app; el script documenta este gap explícitamente y no lo exige. Se abordó también junto con la tarea 5 diferida de P5 (ver P13). Detector de huérfanos: 0 componentes sin importadores (los 47 de `src/lib/components/` están todos en uso). `docs/design-system-refactor-plan.md` (plan anterior, superseded) borrado. `web-player/CLAUDE.md` gana sección "Estilos" con la lista blanca B.4, componentes base a reutilizar y el gap de `strokeWidth` documentado. Línea base recalculada (ver tabla debajo) |
|
||||
|
||||
Decisiones de producto pendientes (preguntar al usuario antes de la parte indicada):
|
||||
- P0/P4: ¿`Button` primary debe verse distinto de secondary?
|
||||
@@ -797,6 +797,24 @@ Decisiones de producto pendientes (preguntar al usuario antes de la parte indica
|
||||
- P10: ¿iconos en los botones de cabecera de álbum **y** playlist?
|
||||
- P12: ¿logo con cuadrado de gradiente también en la Sidebar?
|
||||
|
||||
### D.1 Línea base recalculada (tras P14, cierre del refactor)
|
||||
|
||||
Recalculada con los mismos criterios que A.4 (grep sobre `src/`), en la misma sesión de
|
||||
cierre de P14.
|
||||
|
||||
| Métrica | A.4 (antes) | Tras P14 |
|
||||
| -------------------------------------------------- | ----------- | ------------------------------------------ |
|
||||
| Componentes `.svelte` en `src/lib/components/` | 61 | 47 |
|
||||
| Líneas `.svelte`/`.css`/`.ts` cliente | ~8.700 | ~8.310 (−~390; el objetivo de −1.500/−2.000 ya se había alcanzado en P3–P11, antes de que P13 añadiera `genres.ts`/`search.ts` como código nuevo bien separado) |
|
||||
| Clases arbitrarias `x-[…]` | 44 | 11 (las 11 restantes, en la lista blanca de B.4) |
|
||||
| Espaciados fraccionarios en cuartos (`.25`/`.75`) | ~190 apariciones | 0 (redondeados al entero más cercano, Δ≤1px) |
|
||||
| Radios distintos usados | 13 | 8 (7 tokens B.1.1 + `rounded-full`) |
|
||||
| `strokeWidth` distintos | 13 | 12 (fuera de alcance, ver Registro P14 y CLAUDE.md) |
|
||||
| Atributos `style="…"` | 24 | 16 (en 12 archivos; todos dinámicos por instancia — stagger, posición de menú, `var(--mf-*)`, animación — ninguno con color/medida literal) |
|
||||
| Pluralizaciones a mano `=== 1 ?` | 15 | 2 (1 es la propia `plural()` en `format.ts`; 1 es concordancia de participio en `library` que `plural()` no puede cubrir) |
|
||||
| Errores de `eslint` | 2 | 0 |
|
||||
| `npm run lint:tokens` (nuevo, P14) | — | 0 violaciones |
|
||||
|
||||
---
|
||||
|
||||
## E. Inventario completo (veredicto por archivo)
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||
"gen:api": "npx -y openapi-typescript@7 http://localhost:5111/openapi/v1.json -o src/lib/api/schema.d.ts",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint ."
|
||||
"lint:tokens": "node scripts/check-tokens.mjs",
|
||||
"lint": "prettier --check . && eslint . && npm run lint:tokens"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const files = execSync('git ls-files "src/**/*.svelte"', { encoding: 'utf8' })
|
||||
.split('\n')
|
||||
.filter(Boolean);
|
||||
|
||||
const ARBITRARY_WHITELIST = [
|
||||
/^text-\[clamp\([^)]+\)\]$/, // tipografía fluida (login, Logo)
|
||||
/^leading-\[[\d.]+\]$/, // leading ajustado del display gigante de login
|
||||
/^grid-cols-\[auto_1fr\]$/, // upload: portada + campos
|
||||
/^max-h-\[85dvh\]$/, // Modal
|
||||
/^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-]+\)\]$/
|
||||
];
|
||||
|
||||
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;
|
||||
const QUARTER_STEP_SPACING = /\b[a-z-]+-\d+\.(25|75)\b/g;
|
||||
const HEX_COLOR = /#[0-9a-fA-F]{3,8}\b/g;
|
||||
const RGBA_LITERAL = /\brgba?\(/g;
|
||||
|
||||
let violations = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = readFileSync(file, 'utf8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
lines.forEach((line, i) => {
|
||||
const lineNo = i + 1;
|
||||
|
||||
for (const match of line.matchAll(/[a-zA-Z][a-zA-Z0-9-]*-\[[^\]]+\]/g)) {
|
||||
if (!ARBITRARY_WHITELIST.some((re) => re.test(match[0]))) {
|
||||
violations.push(`${file}:${lineNo} — arbitrario fuera de la lista blanca: \`${match[0]}\``);
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(RAW_ROUNDED)) {
|
||||
violations.push(`${file}:${lineNo} — radio de Tailwind sin tokenizar: \`${match[0]}\``);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(PAIRED_HW)) {
|
||||
violations.push(`${file}:${lineNo} — usa \`size-${match[2]}\` en vez de \`${match[0]}\``);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(QUARTER_STEP_SPACING)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — espaciado fraccionario en cuartos: \`${match[0]}\` (redondear al entero más cercano)`
|
||||
);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(HEX_COLOR)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — color hex literal: \`${match[0]}\` (usa un token --mf-*)`
|
||||
);
|
||||
}
|
||||
|
||||
for (const match of line.matchAll(RGBA_LITERAL)) {
|
||||
violations.push(
|
||||
`${file}:${lineNo} — color rgba() literal: \`${match[0]}\` (usa un token --mf-*)`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error(`\n${violations.length} violación(es) del sistema de diseño:\n`);
|
||||
for (const v of violations) console.error(` ${v}`);
|
||||
console.error('\nVer docs/frontend-refactor-plan.md, sección B.4.\n');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('check-tokens: sin violaciones.');
|
||||
@@ -3,6 +3,13 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<script>
|
||||
try {
|
||||
if (localStorage.getItem('musify.theme') === 'light') {
|
||||
document.documentElement.dataset.theme = 'light';
|
||||
}
|
||||
} catch {}
|
||||
</script>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user