From dc1ba89a6d17b0035aedc07a72812ee28846c18b Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 14 Sep 2026 23:42:45 +0200 Subject: [PATCH] Add likes, follows and playlist visibility; redesign web-player UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - New Likes feature (toggle/get liked tracks) and endpoints - User follow/unfollow, listening stats, public user profile - Playlist visibility (public/private) support - Various auth and DTO/response adjustments to support the above Frontend (web-player): - New design system pass: theme tokens/mode (light/dark), glass menu, tabs bar, player dock/extras/queue components, mix tiles, artist link - New routes: Liked, public user profile (/u/[id]), likes API proxy - Search refactor: shared SearchBox component used by TopBar and MobileHeader (search now reachable on mobile), live results as you type instead of requiring Enter, dropped the "Buscando…" quick-result preview - Removed now-unused components (MobileNav, CoverBadge, MixBentoTile, PlayPauseOverlay, SuggestionsTable, userDirectory) - Design handoff reference docs added under web-player/design_handoff_musify_player and web-player/docs/design-system-refactor-plan.md --- .claude/launch.json | 11 +- .../Responses/AlbumApplicationResponse.cs | 3 +- .../Musify.Application/Contracts/IDatabase.cs | 4 + .../Likes/GetLikedTracks.cs | 45 + .../Likes/ToggleTrackLike.cs | 83 + .../PlayLists/CreatePlayList.cs | 6 +- .../PlayLists/GetPlayListById.cs | 7 +- .../PlayLists/GetPlayListsByUserId.cs | 8 +- .../Responses/PlayListApplicationResponse.cs | 3 + .../PlayLists/UpdatePlayList.cs | 8 +- .../Serialization/LongAsStringConverter.cs | 44 + .../LocalTrackApplicationResponse.cs | 4 +- .../Musify.Application/Users/FollowUser.cs | 50 + .../Users/GetListeningStats.cs | 48 + .../Users/GetUserProfile.cs | 63 + .../Users/IsFollowingUser.cs | 17 + .../Responses/UserApplicationResponse.cs | 6 +- .../Musify.Application/Users/UnfollowUser.cs | 32 + .../Core/Musify.Domain/Entities/PlayList.cs | 3 + .../Core/Musify.Domain/Entities/TrackLike.cs | 28 + .../Core/Musify.Domain/Entities/UserFollow.cs | 28 + .../ValueObjects/PlaylistVisibility.cs | 8 + .../Assets/PlayListDefaultCover.png | Bin 10489 -> 70143 bytes .../Persistence/Database.cs | 29 + ...14144913_AddPlayListVisibility.Designer.cs | 1245 ++++++++ .../20260914144913_AddPlayListVisibility.cs | 29 + .../20260914150045_AddTrackLikes.Designer.cs | 1289 ++++++++ .../20260914150045_AddTrackLikes.cs | 59 + .../20260914151900_AddUserFollows.Designer.cs | 1333 ++++++++ .../20260914151900_AddUserFollows.cs | 59 + .../Migrations/DatabaseModelSnapshot.cs | 93 + .../Services/YouTubeMusicService.cs | 15 +- .../AuthenticationExtensions.cs | 1 + .../Authentication/JwtBearerEventsHandler.cs | 74 +- .../Likes/ToggleLikeRequest.cs | 8 + .../PlayLists/CreatePlayListRequest.cs | 5 +- .../PlayLists/UpdatePlayListRequest.cs | 5 +- .../Musify.Api/Endpoints/LikeEndpoints.cs | 61 + .../Musify.Api/Endpoints/PlayListEndpoints.cs | 12 +- .../Musify.Api/Endpoints/UserEndpoints.cs | 81 + .../Musify.Api/OpenApi/OpenApiOptionsSetup.cs | 19 + backend/Hosts/Musify.Api/Program.cs | 1 + .../Likes/ToggleLikeRequestValidator.cs | 27 + .../Likes/GetLikedTracksQueryHandlerTests.cs | 55 + .../ToggleTrackLikeCommandHandlerTests.cs | 103 + .../CreatePlayListCommandHandlerTests.cs | 28 + .../GetPlayListByIdQueryHandlerTests.cs | 51 +- .../GetPlayListsByUserIdQueryHandlerTests.cs | 16 + .../UpdatePlayListCommandHandlerTests.cs | 30 + .../TestSupport/TestDatabase.cs | 24 + .../TestSupport/TestEntities.cs | 6 +- .../Users/FollowUserCommandHandlerTests.cs | 61 + .../GetListeningStatsQueryHandlerTests.cs | 87 + .../Users/GetUserProfileQueryHandlerTests.cs | 41 + .../Users/IsFollowingUserQueryHandlerTests.cs | 36 + .../Users/UnfollowUserCommandHandlerTests.cs | 38 + .../Musify.dc.html | 2801 +++++++++++++++++ .../design_handoff_musify_player/README.md | 325 ++ .../screenshots/01-home-hero.png | Bin 0 -> 32701 bytes .../02-home-continuar-spotlight.png | Bin 0 -> 32712 bytes .../screenshots/03-home-mixes-artistas.png | Bin 0 -> 32697 bytes .../04-home-populares-playlists.png | Bin 0 -> 32691 bytes .../screenshots/05-descubrir.png | Bin 0 -> 34053 bytes .../screenshots/06-playlists.png | Bin 0 -> 37419 bytes .../screenshots/07-tracklist-me-gusta.png | Bin 0 -> 32766 bytes .../screenshots/08-sin-resultados.png | Bin 0 -> 28546 bytes .../design_handoff_musify_player/support.js | 1953 ++++++++++++ .../docs/design-system-refactor-plan.md | 335 ++ web-player/eslint.config.js | 8 +- web-player/package-lock.json | 20 + web-player/package.json | 2 + web-player/src/lib/api/schema.d.ts | 375 ++- .../lib/components/AlbumContextMenu.svelte | 26 +- .../src/lib/components/MobileHeader.svelte | 141 +- .../src/lib/components/MobileNav.svelte | 53 - .../src/lib/components/SearchBox.svelte | 86 + web-player/src/lib/components/Sidebar.svelte | 180 +- web-player/src/lib/components/TabsBar.svelte | 50 + web-player/src/lib/components/TopBar.svelte | 266 +- .../lib/components/TrackContextMenu.svelte | 20 +- .../lib/components/player/PlayerBar.svelte | 47 +- .../lib/components/player/PlayerDock.svelte | 29 + .../lib/components/player/PlayerExtras.svelte | 22 + .../src/lib/components/player/Queue.svelte | 117 + .../src/lib/components/player/SeekBar.svelte | 19 +- .../lib/components/player/TrackInfo.svelte | 53 +- .../player/TransportControls.svelte | 42 +- .../components/player/VolumeControl.svelte | 25 +- .../src/lib/components/ui/AlbumForm.svelte | 18 +- web-player/src/lib/components/ui/Alert.svelte | 8 +- .../src/lib/components/ui/ArtistAvatar.svelte | 17 +- .../src/lib/components/ui/ArtistLink.svelte | 23 + .../src/lib/components/ui/Button.svelte | 18 +- .../src/lib/components/ui/Checkbox.svelte | 4 +- .../src/lib/components/ui/CoverBadge.svelte | 14 - .../src/lib/components/ui/EmptyState.svelte | 19 +- .../src/lib/components/ui/EqBars.svelte | 23 + web-player/src/lib/components/ui/Field.svelte | 16 +- .../src/lib/components/ui/GlassMenu.svelte | 15 + .../lib/components/ui/ImageDropzone.svelte | 17 +- web-player/src/lib/components/ui/Input.svelte | 2 +- .../src/lib/components/ui/MediaCard.svelte | 4 +- .../src/lib/components/ui/MenuItem.svelte | 17 +- .../src/lib/components/ui/MixBentoTile.svelte | 66 - .../src/lib/components/ui/MixTile.svelte | 51 + web-player/src/lib/components/ui/Modal.svelte | 22 +- .../src/lib/components/ui/NowPlaying.svelte | 13 +- web-player/src/lib/components/ui/Page.svelte | 2 +- .../src/lib/components/ui/PageHeader.svelte | 6 +- .../lib/components/ui/PlayPauseOverlay.svelte | 22 - .../src/lib/components/ui/PlaylistCard.svelte | 8 +- .../src/lib/components/ui/PlaylistForm.svelte | 93 +- .../lib/components/ui/SectionHeading.svelte | 26 +- .../src/lib/components/ui/Surface.svelte | 2 +- .../src/lib/components/ui/Textarea.svelte | 2 +- .../lib/components/ui/TrackIndexCell.svelte | 6 +- .../src/lib/components/ui/TrackMeta.svelte | 2 +- .../src/lib/components/ui/TrackRow.svelte | 7 +- .../src/lib/components/ui/TrackTable.svelte | 10 +- .../lib/components/ui/TrackTitleCell.svelte | 31 +- web-player/src/lib/config.ts | 11 +- web-player/src/lib/format.ts | 7 + web-player/src/lib/player/liked.svelte.ts | 95 + web-player/src/lib/player/player.svelte.ts | 47 +- .../src/lib/player/queuePanel.svelte.ts | 16 + web-player/src/lib/playlists.svelte.ts | 13 + web-player/src/lib/recentlyPlayed.ts | 42 + web-player/src/lib/server/greeting.ts | 125 +- web-player/src/lib/server/userDirectory.ts | 24 - web-player/src/lib/server/youtube.ts | 2 +- web-player/src/lib/theme/color.ts | 4 +- web-player/src/lib/theme/mode.svelte.ts | 40 + web-player/src/lib/theme/theme.css | 157 +- web-player/src/lib/theme/tokens.ts | 86 + web-player/src/lib/tracks.ts | 7 +- .../src/routes/(app)/albums/+page.svelte | 7 +- .../routes/(app)/albums/[id]/+page.server.ts | 2 +- .../albums/[id]/components/AlbumHeader.svelte | 12 +- .../[id]/components/AlbumTrackTable.svelte | 12 +- .../[id]/components/LibraryPicker.svelte | 5 +- .../[source]/[externalId]/+page.svelte | 26 +- .../src/routes/(app)/explore/+page.server.ts | 22 +- .../src/routes/(app)/explore/+page.svelte | 322 +- .../src/routes/(app)/library/+page.svelte | 16 +- .../src/routes/(app)/liked/+page.server.ts | 7 + .../src/routes/(app)/liked/+page.svelte | 136 + .../src/routes/(app)/mixes/[id]/+page.svelte | 26 +- .../routes/(app)/playlists/+page.server.ts | 28 +- .../src/routes/(app)/playlists/+page.svelte | 66 +- .../(app)/playlists/[id]/+page.server.ts | 58 +- .../routes/(app)/playlists/[id]/+page.svelte | 34 +- .../[id]/components/PlaylistHeader.svelte | 36 +- .../[id]/components/PlaylistTrackTable.svelte | 23 +- .../[id]/components/SuggestionsTable.svelte | 151 - .../src/routes/(app)/u/[id]/+page.server.ts | 75 + .../src/routes/(app)/u/[id]/+page.svelte | 100 + .../src/routes/(app)/upload/+page.svelte | 85 +- web-player/src/routes/+error.svelte | 16 + web-player/src/routes/+layout.server.ts | 64 +- web-player/src/routes/+layout.svelte | 126 +- web-player/src/routes/+page.server.ts | 76 +- web-player/src/routes/+page.svelte | 872 +++-- web-player/src/routes/api/likes/+server.ts | 16 + web-player/src/routes/layout.css | 31 +- web-player/src/routes/login/+page.svelte | 15 +- web-player/vite.config.ts | 3 + 166 files changed, 14271 insertions(+), 1982 deletions(-) create mode 100644 backend/Core/Musify.Application/Likes/GetLikedTracks.cs create mode 100644 backend/Core/Musify.Application/Likes/ToggleTrackLike.cs create mode 100644 backend/Core/Musify.Application/Serialization/LongAsStringConverter.cs create mode 100644 backend/Core/Musify.Application/Users/FollowUser.cs create mode 100644 backend/Core/Musify.Application/Users/GetListeningStats.cs create mode 100644 backend/Core/Musify.Application/Users/GetUserProfile.cs create mode 100644 backend/Core/Musify.Application/Users/IsFollowingUser.cs create mode 100644 backend/Core/Musify.Application/Users/UnfollowUser.cs create mode 100644 backend/Core/Musify.Domain/Entities/TrackLike.cs create mode 100644 backend/Core/Musify.Domain/Entities/UserFollow.cs create mode 100644 backend/Core/Musify.Domain/ValueObjects/PlaylistVisibility.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.Designer.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.Designer.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.Designer.cs create mode 100644 backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.cs create mode 100644 backend/Hosts/Musify.Api/DataTransferObjects/Likes/ToggleLikeRequest.cs create mode 100644 backend/Hosts/Musify.Api/Endpoints/LikeEndpoints.cs create mode 100644 backend/Hosts/Musify.Api/Validators/Likes/ToggleLikeRequestValidator.cs create mode 100644 backend/Tests/Musify.Application.Tests/Likes/GetLikedTracksQueryHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Likes/ToggleTrackLikeCommandHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Users/FollowUserCommandHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Users/GetListeningStatsQueryHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Users/GetUserProfileQueryHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Users/IsFollowingUserQueryHandlerTests.cs create mode 100644 backend/Tests/Musify.Application.Tests/Users/UnfollowUserCommandHandlerTests.cs create mode 100644 web-player/design_handoff_musify_player/Musify.dc.html create mode 100644 web-player/design_handoff_musify_player/README.md create mode 100644 web-player/design_handoff_musify_player/screenshots/01-home-hero.png create mode 100644 web-player/design_handoff_musify_player/screenshots/02-home-continuar-spotlight.png create mode 100644 web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png create mode 100644 web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png create mode 100644 web-player/design_handoff_musify_player/screenshots/05-descubrir.png create mode 100644 web-player/design_handoff_musify_player/screenshots/06-playlists.png create mode 100644 web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png create mode 100644 web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png create mode 100644 web-player/design_handoff_musify_player/support.js create mode 100644 web-player/docs/design-system-refactor-plan.md delete mode 100644 web-player/src/lib/components/MobileNav.svelte create mode 100644 web-player/src/lib/components/SearchBox.svelte create mode 100644 web-player/src/lib/components/TabsBar.svelte create mode 100644 web-player/src/lib/components/player/PlayerDock.svelte create mode 100644 web-player/src/lib/components/player/PlayerExtras.svelte create mode 100644 web-player/src/lib/components/player/Queue.svelte create mode 100644 web-player/src/lib/components/ui/ArtistLink.svelte delete mode 100644 web-player/src/lib/components/ui/CoverBadge.svelte create mode 100644 web-player/src/lib/components/ui/EqBars.svelte create mode 100644 web-player/src/lib/components/ui/GlassMenu.svelte delete mode 100644 web-player/src/lib/components/ui/MixBentoTile.svelte create mode 100644 web-player/src/lib/components/ui/MixTile.svelte delete mode 100644 web-player/src/lib/components/ui/PlayPauseOverlay.svelte create mode 100644 web-player/src/lib/player/liked.svelte.ts create mode 100644 web-player/src/lib/player/queuePanel.svelte.ts create mode 100644 web-player/src/lib/playlists.svelte.ts create mode 100644 web-player/src/lib/recentlyPlayed.ts delete mode 100644 web-player/src/lib/server/userDirectory.ts create mode 100644 web-player/src/lib/theme/mode.svelte.ts create mode 100644 web-player/src/lib/theme/tokens.ts create mode 100644 web-player/src/routes/(app)/liked/+page.server.ts create mode 100644 web-player/src/routes/(app)/liked/+page.svelte delete mode 100644 web-player/src/routes/(app)/playlists/[id]/components/SuggestionsTable.svelte create mode 100644 web-player/src/routes/(app)/u/[id]/+page.server.ts create mode 100644 web-player/src/routes/(app)/u/[id]/+page.svelte create mode 100644 web-player/src/routes/+error.svelte create mode 100644 web-player/src/routes/api/likes/+server.ts diff --git a/.claude/launch.json b/.claude/launch.json index dd00e9b..f641b22 100644 --- a/.claude/launch.json +++ b/.claude/launch.json @@ -4,14 +4,21 @@ { "name": "musify-web", "runtimeExecutable": "npm", - "runtimeArgs": ["--prefix", "web-player", "run", "dev", "--", "--port", "3000"], - "port": 3000 + "runtimeArgs": ["--prefix", "web-player", "run", "dev"], + "port": 3000, + "autoPort": true }, { "name": "musify-api", "runtimeExecutable": "dotnet", "runtimeArgs": ["run", "--project", "backend/Hosts/Musify.Api"], "port": 5111 + }, + { + "name": "musify-streaming-gateway", + "runtimeExecutable": "dotnet", + "runtimeArgs": ["run", "--project", "backend/Hosts/Musify.StreamingGateway"], + "port": 8081 } ] } diff --git a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs index 0323ded..1cd4fa9 100644 --- a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs +++ b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs @@ -1,5 +1,6 @@ using System.Text.Json.Serialization; using Musify.Domain.Entities; +using Musify.Application.Serialization; namespace Musify.Application.Albums.Responses { @@ -17,7 +18,7 @@ namespace Musify.Application.Albums.Responses string Title, string? Description, int? ReleaseYear, - long OwnerUserId, + [property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId, int TrackCount, string? SmallImageKeyName, string? MediumImageKeyName, diff --git a/backend/Core/Musify.Application/Contracts/IDatabase.cs b/backend/Core/Musify.Application/Contracts/IDatabase.cs index a278e0c..ef100ae 100644 --- a/backend/Core/Musify.Application/Contracts/IDatabase.cs +++ b/backend/Core/Musify.Application/Contracts/IDatabase.cs @@ -41,6 +41,10 @@ namespace Musify.Application.Contracts DbSet ListeningHistories { get; } + DbSet TrackLikes { get; } + + DbSet UserFollows { get; } + Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken); Task SaveChangesAsync(CancellationToken cancellationToken); diff --git a/backend/Core/Musify.Application/Likes/GetLikedTracks.cs b/backend/Core/Musify.Application/Likes/GetLikedTracks.cs new file mode 100644 index 0000000..10f5abb --- /dev/null +++ b/backend/Core/Musify.Application/Likes/GetLikedTracks.cs @@ -0,0 +1,45 @@ +using ErrorOr; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; +using Musify.Application.Shared; +using Musify.Application.Tracks.Responses; +using Musify.Domain.Entities; +using X.PagedList; +using X.PagedList.EF; + +namespace Musify.Application.Likes +{ + public record GetLikedTracksQuery( + long UserId, + int PageNumber, + int PageSize) + : IQuery>>; + + public class GetLikedTracksQueryHandler(IDatabase database) + : IQueryHandler>> + { + public async ValueTask>> Handle(GetLikedTracksQuery request, CancellationToken cancellationToken) + { + var likesQuery = database.TrackLikes + .AsNoTracking() + .Include(like => ((ExternalTrack)like.Track).TrackArtists) + .ThenInclude(trackArtist => trackArtist.Artist) + .Include(like => ((LocalTrack)like.Track).Owner) + .Where(like => like.UserId == request.UserId); + + var totalCount = await likesQuery.CountAsync(cancellationToken); + + var pagedEntities = await likesQuery + .OrderByDescending(like => like.CreatedAt) + .Select(like => new { like.Track, ListensCount = like.Track.ListeningHistories.Count }) + .ToPagedListAsync(request.PageNumber, request.PageSize, totalCount, cancellationToken); + + var pagedTracks = new StaticPagedList( + pagedEntities.Select(x => TrackApplicationResponse.FromEntity(x.Track, x.ListensCount)), + pagedEntities.PageNumber, pagedEntities.PageSize, pagedEntities.TotalItemCount); + + return PaginatedResponse.FromPagedList(pagedTracks); + } + } +} diff --git a/backend/Core/Musify.Application/Likes/ToggleTrackLike.cs b/backend/Core/Musify.Application/Likes/ToggleTrackLike.cs new file mode 100644 index 0000000..3c84f0c --- /dev/null +++ b/backend/Core/Musify.Application/Likes/ToggleTrackLike.cs @@ -0,0 +1,83 @@ +using ErrorOr; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Application.Services; +using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; + +namespace Musify.Application.Likes +{ + public record ToggleTrackLikeCommand( + long UserId, + Guid? TrackId, + TrackSource? Source, + string? ExternalId) + : ICommand>; + + public class ToggleTrackLikeCommandHandler( + IDatabase database, + YouTubeTrackProvisioner provisioner, + ILogger logger) + : ICommandHandler> + { + public async ValueTask> Handle(ToggleTrackLikeCommand request, CancellationToken cancellationToken) + { + var trackIdResult = await ResolveTrackIdAsync(request, cancellationToken); + if (trackIdResult.IsError) + return trackIdResult.Errors; + + var trackId = trackIdResult.Value; + + var existingLike = await database.TrackLikes + .SingleOrDefaultAsync(like => like.UserId == request.UserId && like.TrackId == trackId, cancellationToken); + + bool nowLiked; + if (existingLike is not null) + { + database.TrackLikes.Remove(existingLike); + nowLiked = false; + } + else + { + await database.TrackLikes.AddAsync(new TrackLike + { + UserId = request.UserId, + TrackId = trackId + }, cancellationToken); + nowLiked = true; + } + + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("User {UserId} {Action} track {TrackId}", + request.UserId, nowLiked ? "liked" : "unliked", trackId); + + return nowLiked; + } + + private async ValueTask> ResolveTrackIdAsync(ToggleTrackLikeCommand request, CancellationToken cancellationToken) + { + if (request.TrackId.HasValue) + { + var trackExists = await database.Tracks + .AsNoTracking() + .AnyAsync(t => t.Id == request.TrackId.Value, cancellationToken); + if (!trackExists) + { + logger.LogInformation("Track {TrackId} not found", request.TrackId); + return Error.NotFound(description: $"Track {request.TrackId} not found"); + } + + return request.TrackId.Value; + } + + var provisionResult = await provisioner.GetOrCreateAsync(request.ExternalId!, cancellationToken); + if (provisionResult.IsError) + return provisionResult.Errors; + + return provisionResult.Value.Track.Id; + } + } +} diff --git a/backend/Core/Musify.Application/PlayLists/CreatePlayList.cs b/backend/Core/Musify.Application/PlayLists/CreatePlayList.cs index e50a91c..e0598c7 100644 --- a/backend/Core/Musify.Application/PlayLists/CreatePlayList.cs +++ b/backend/Core/Musify.Application/PlayLists/CreatePlayList.cs @@ -17,7 +17,8 @@ namespace Musify.Application.PlayLists long UserId, string Name, string? Description, - Guid? PictureIntentId) + Guid? PictureIntentId, + PlaylistVisibility Visibility = PlaylistVisibility.Private) : ICommand>; public class CreatePlayListCommandHandler( @@ -55,7 +56,8 @@ namespace Musify.Application.PlayLists Name = request.Name, NormalizedName = request.Name.Trim().ToUpperInvariant(), Description = string.IsNullOrWhiteSpace(request.Description)? null: request.Description.Trim(), - Pictures = resolvedPictures.Pictures + Pictures = resolvedPictures.Pictures, + Visibility = request.Visibility }; await database.PlayLists.AddAsync(playList, cancellationToken); diff --git a/backend/Core/Musify.Application/PlayLists/GetPlayListById.cs b/backend/Core/Musify.Application/PlayLists/GetPlayListById.cs index d5afb0f..71b87bc 100644 --- a/backend/Core/Musify.Application/PlayLists/GetPlayListById.cs +++ b/backend/Core/Musify.Application/PlayLists/GetPlayListById.cs @@ -3,10 +3,11 @@ using Microsoft.EntityFrameworkCore; using Musify.Application.PlayLists.Responses; using Mediator; using Musify.Application.Contracts; +using Musify.Domain.ValueObjects; namespace Musify.Application.PlayLists { - public record GetPlayListByIdQuery(Guid Id) + public record GetPlayListByIdQuery(Guid Id, long? RequestingUserId = null) : IQuery>; public class GetPlayListByIdQueryHandler(IDatabase database) @@ -31,6 +32,10 @@ namespace Musify.Application.PlayLists if (entry is null) return Error.NotFound(); + if (entry.PlayList.Visibility == PlaylistVisibility.Private + && entry.PlayList.UserId != request.RequestingUserId) + return Error.NotFound(); + return PlayListApplicationResponse.FromEntity(entry.PlayList, entry.CoverTrackIds); } } diff --git a/backend/Core/Musify.Application/PlayLists/GetPlayListsByUserId.cs b/backend/Core/Musify.Application/PlayLists/GetPlayListsByUserId.cs index ec1aa93..99a425b 100644 --- a/backend/Core/Musify.Application/PlayLists/GetPlayListsByUserId.cs +++ b/backend/Core/Musify.Application/PlayLists/GetPlayListsByUserId.cs @@ -14,7 +14,8 @@ namespace Musify.Application.PlayLists long UserId, string? Name, int PageNumber, - int PageSize) + int PageSize, + bool OnlyPublic = false) : IQuery> >; public class GetPlayListsByUserIdQueryHandler(IDatabase database) @@ -26,6 +27,11 @@ namespace Musify.Application.PlayLists .AsNoTracking() .Where(p => p.UserId == request.UserId); + if (request.OnlyPublic) + { + playListsQuery = playListsQuery.Where(p => p.Visibility == PlaylistVisibility.Public); + } + if (!string.IsNullOrEmpty(request.Name)) { var normalizedName = request.Name.Trim().ToUpperInvariant(); diff --git a/backend/Core/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs b/backend/Core/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs index 4a1fb93..225c5d2 100644 --- a/backend/Core/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs +++ b/backend/Core/Musify.Application/PlayLists/Responses/PlayListApplicationResponse.cs @@ -1,4 +1,5 @@ using Musify.Domain.Entities; +using Musify.Domain.ValueObjects; namespace Musify.Application.PlayLists.Responses { @@ -9,6 +10,7 @@ namespace Musify.Application.PlayLists.Responses string? SmallImageKeyName, string? MediumImageKeyName, string? LargeImageKeyName, + PlaylistVisibility Visibility, DateTime CreatedAt, DateTime UpdatedAt, IReadOnlyList CoverTrackIds) @@ -26,6 +28,7 @@ namespace Musify.Application.PlayLists.Responses playList.Pictures?.SmallName, playList.Pictures?.MediumName, playList.Pictures?.LargeName, + playList.Visibility, playList.CreatedAt, playList.UpdatedAt, coverTrackIds ?? []); diff --git a/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs b/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs index 1d54615..3d97bdd 100644 --- a/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs +++ b/backend/Core/Musify.Application/PlayLists/UpdatePlayList.cs @@ -18,7 +18,8 @@ namespace Musify.Application.PlayLists Guid PlayListId, string? NewName, string? NewDescription, - Guid? NewPictureIntentId) + Guid? NewPictureIntentId, + PlaylistVisibility? NewVisibility = null) : ICommand>; public class UpdatePlayListCommandHandler( @@ -57,6 +58,11 @@ namespace Musify.Application.PlayLists playListEntity.Description = request.NewDescription; } + if (request.NewVisibility.HasValue) + { + playListEntity.Visibility = request.NewVisibility.Value; + } + UploadIntent? pictureIntent = null; string? finalPictureKey = null; diff --git a/backend/Core/Musify.Application/Serialization/LongAsStringConverter.cs b/backend/Core/Musify.Application/Serialization/LongAsStringConverter.cs new file mode 100644 index 0000000..0dd3526 --- /dev/null +++ b/backend/Core/Musify.Application/Serialization/LongAsStringConverter.cs @@ -0,0 +1,44 @@ +using System.Globalization; +using System.Text.Json; +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 +{ + public override long Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => + reader.TokenType == JsonTokenType.String + ? long.Parse(reader.GetString()!, CultureInfo.InvariantCulture) + : reader.GetInt64(); + + public override void Write(Utf8JsonWriter writer, long value, JsonSerializerOptions options) => + writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); +} + +public sealed class NullableLongAsStringConverter : JsonConverter +{ + public override long? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + return reader.TokenType == JsonTokenType.String + ? string.IsNullOrEmpty(reader.GetString()) ? null : long.Parse(reader.GetString()!, CultureInfo.InvariantCulture) + : reader.GetInt64(); + } + + public override void Write(Utf8JsonWriter writer, long? value, JsonSerializerOptions options) + { + if (value.HasValue) + writer.WriteStringValue(value.Value.ToString(CultureInfo.InvariantCulture)); + else + writer.WriteNullValue(); + } +} diff --git a/backend/Core/Musify.Application/Tracks/Responses/LocalTrackApplicationResponse.cs b/backend/Core/Musify.Application/Tracks/Responses/LocalTrackApplicationResponse.cs index 6af6f00..09580d3 100644 --- a/backend/Core/Musify.Application/Tracks/Responses/LocalTrackApplicationResponse.cs +++ b/backend/Core/Musify.Application/Tracks/Responses/LocalTrackApplicationResponse.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; +using Musify.Application.Serialization; using Musify.Domain.Entities; using Musify.Domain.ValueObjects; @@ -12,7 +14,7 @@ namespace Musify.Application.Tracks.Responses int ListensCount, DateTime CreatedAt, DateTime UpdatedAt, - long OwnerUserId) + [property: JsonConverter(typeof(LongAsStringConverter))] long OwnerUserId) : TrackApplicationResponse(Id, Title, Artist, AudioStatus, Duration, ListensCount, CreatedAt, UpdatedAt) { public static LocalTrackApplicationResponse FromEntity(LocalTrack track, int listensCount) => diff --git a/backend/Core/Musify.Application/Users/FollowUser.cs b/backend/Core/Musify.Application/Users/FollowUser.cs new file mode 100644 index 0000000..c4d6541 --- /dev/null +++ b/backend/Core/Musify.Application/Users/FollowUser.cs @@ -0,0 +1,50 @@ +using ErrorOr; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; +using Musify.Domain.Entities; + +namespace Musify.Application.Users +{ + public record FollowUserCommand(long FollowerId, long FollowedId) : ICommand>; + + public class FollowUserCommandHandler( + IDatabase database, + ILogger logger) + : ICommandHandler> + { + public async ValueTask> Handle(FollowUserCommand request, CancellationToken cancellationToken) + { + if (request.FollowerId == request.FollowedId) + return Error.Validation(description: "A user cannot follow themselves."); + + var followedExists = await database.Users + .AsNoTracking() + .AnyAsync(u => u.Id == request.FollowedId, cancellationToken); + if (!followedExists) + { + logger.LogInformation("User {FollowedId} not found", request.FollowedId); + return Error.NotFound(description: $"User {request.FollowedId} not found"); + } + + var alreadyFollowing = await database.UserFollows + .AsNoTracking() + .AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken); + if (alreadyFollowing) + return new Success(); + + await database.UserFollows.AddAsync(new UserFollow + { + FollowerId = request.FollowerId, + FollowedId = request.FollowedId + }, cancellationToken); + + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("User {FollowerId} followed user {FollowedId}", request.FollowerId, request.FollowedId); + + return new Success(); + } + } +} diff --git a/backend/Core/Musify.Application/Users/GetListeningStats.cs b/backend/Core/Musify.Application/Users/GetListeningStats.cs new file mode 100644 index 0000000..1eefb36 --- /dev/null +++ b/backend/Core/Musify.Application/Users/GetListeningStats.cs @@ -0,0 +1,48 @@ +using Mediator; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; + +namespace Musify.Application.Users +{ + public record GetListeningStatsQuery(long UserId) : IQuery; + + public record ListeningStatsResponse(int TracksThisWeek, int SecondsThisWeek, int StreakDays); + + public class GetListeningStatsQueryHandler(IDatabase database) + : IQueryHandler + { + public async ValueTask Handle(GetListeningStatsQuery request, CancellationToken cancellationToken) + { + var weekAgo = DateTime.UtcNow.AddDays(-7); + + var thisWeekHistory = database.ListeningHistories + .Where(l => l.UserId == request.UserId && l.ListenedAt >= weekAgo); + + var tracksThisWeek = await thisWeekHistory + .Select(l => l.TrackId) + .Distinct() + .CountAsync(cancellationToken); + + var secondsThisWeek = await thisWeekHistory + .Join(database.Tracks, l => l.TrackId, t => t.Id, (l, t) => t.DurationSeconds) + .SumAsync(cancellationToken); + + var listenedDates = await database.ListeningHistories + .Where(l => l.UserId == request.UserId) + .Select(l => l.ListenedAt.Date) + .Distinct() + .ToListAsync(cancellationToken); + + var listenedDateSet = listenedDates.ToHashSet(); + var streakDays = 0; + var day = DateTime.UtcNow.Date; + while (listenedDateSet.Contains(day)) + { + streakDays++; + day = day.AddDays(-1); + } + + return new ListeningStatsResponse(tracksThisWeek, (int)Math.Round(secondsThisWeek), streakDays); + } + } +} diff --git a/backend/Core/Musify.Application/Users/GetUserProfile.cs b/backend/Core/Musify.Application/Users/GetUserProfile.cs new file mode 100644 index 0000000..f697448 --- /dev/null +++ b/backend/Core/Musify.Application/Users/GetUserProfile.cs @@ -0,0 +1,63 @@ +using System.Text.Json.Serialization; +using ErrorOr; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; +using Musify.Application.Serialization; + +namespace Musify.Application.Users +{ + public record GetUserProfileQuery(long UserId) : IQuery>; + + public record UserProfileResponse( + [property: JsonConverter(typeof(LongAsStringConverter))] long Id, + string Name, + string? FirstName, + string? SecondName, + string? ProfilePictureUrl, + DateTime CreatedAt, + int FollowersCount, + int FollowingCount); + + public class GetUserProfileQueryHandler(IDatabase database) + : IQueryHandler> + { + public async ValueTask> Handle(GetUserProfileQuery request, CancellationToken cancellationToken) + { + var user = await database.Users + .AsNoTracking() + .Where(u => u.Id == request.UserId) + .Select(u => new + { + u.Id, + u.Name, + u.FirstName, + u.SecondName, + u.ProfilePictureUrl, + u.CreatedAt + }) + .SingleOrDefaultAsync(cancellationToken); + + if (user is null) + return Error.NotFound(); + + var followersCount = await database.UserFollows + .AsNoTracking() + .CountAsync(f => f.FollowedId == request.UserId, cancellationToken); + + var followingCount = await database.UserFollows + .AsNoTracking() + .CountAsync(f => f.FollowerId == request.UserId, cancellationToken); + + return new UserProfileResponse( + user.Id, + user.Name, + user.FirstName, + user.SecondName, + user.ProfilePictureUrl, + user.CreatedAt, + followersCount, + followingCount); + } + } +} diff --git a/backend/Core/Musify.Application/Users/IsFollowingUser.cs b/backend/Core/Musify.Application/Users/IsFollowingUser.cs new file mode 100644 index 0000000..a8e9a2a --- /dev/null +++ b/backend/Core/Musify.Application/Users/IsFollowingUser.cs @@ -0,0 +1,17 @@ +using Mediator; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; + +namespace Musify.Application.Users +{ + public record IsFollowingUserQuery(long FollowerId, long FollowedId) : IQuery; + + public class IsFollowingUserQueryHandler(IDatabase database) + : IQueryHandler + { + public ValueTask Handle(IsFollowingUserQuery request, CancellationToken cancellationToken) => + new(database.UserFollows + .AsNoTracking() + .AnyAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken)); + } +} diff --git a/backend/Core/Musify.Application/Users/Responses/UserApplicationResponse.cs b/backend/Core/Musify.Application/Users/Responses/UserApplicationResponse.cs index bde3649..9bd442f 100644 --- a/backend/Core/Musify.Application/Users/Responses/UserApplicationResponse.cs +++ b/backend/Core/Musify.Application/Users/Responses/UserApplicationResponse.cs @@ -1,9 +1,11 @@ -using Musify.Domain.Entities; +using System.Text.Json.Serialization; +using Musify.Application.Serialization; +using Musify.Domain.Entities; namespace Musify.Application.Users.Responses { public record UserApplicationResponse( - long Id, + [property: JsonConverter(typeof(LongAsStringConverter))] long Id, string Name, string? FirstName, string? SecondName, diff --git a/backend/Core/Musify.Application/Users/UnfollowUser.cs b/backend/Core/Musify.Application/Users/UnfollowUser.cs new file mode 100644 index 0000000..b960c64 --- /dev/null +++ b/backend/Core/Musify.Application/Users/UnfollowUser.cs @@ -0,0 +1,32 @@ +using ErrorOr; +using Mediator; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Musify.Application.Contracts; + +namespace Musify.Application.Users +{ + public record UnfollowUserCommand(long FollowerId, long FollowedId) : ICommand>; + + public class UnfollowUserCommandHandler( + IDatabase database, + ILogger logger) + : ICommandHandler> + { + public async ValueTask> Handle(UnfollowUserCommand request, CancellationToken cancellationToken) + { + var follow = await database.UserFollows + .SingleOrDefaultAsync(f => f.FollowerId == request.FollowerId && f.FollowedId == request.FollowedId, cancellationToken); + + if (follow is null) + return new Success(); + + database.UserFollows.Remove(follow); + await database.SaveChangesAsync(cancellationToken); + + logger.LogInformation("User {FollowerId} unfollowed user {FollowedId}", request.FollowerId, request.FollowedId); + + return new Success(); + } + } +} diff --git a/backend/Core/Musify.Domain/Entities/PlayList.cs b/backend/Core/Musify.Domain/Entities/PlayList.cs index 1922ef8..584fbb3 100644 --- a/backend/Core/Musify.Domain/Entities/PlayList.cs +++ b/backend/Core/Musify.Domain/Entities/PlayList.cs @@ -33,6 +33,9 @@ namespace Musify.Domain.Entities [Required] public LifeCycleStatus LifeCycleStatus { get; set; } = LifeCycleStatus.Active; + [Required] + public PlaylistVisibility Visibility { get; set; } = PlaylistVisibility.Private; + [Required] public DateTime CreatedAt { get; set; } = DateTime.UtcNow; diff --git a/backend/Core/Musify.Domain/Entities/TrackLike.cs b/backend/Core/Musify.Domain/Entities/TrackLike.cs new file mode 100644 index 0000000..41ebb9f --- /dev/null +++ b/backend/Core/Musify.Domain/Entities/TrackLike.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("TrackLikes")] + public class TrackLike + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required long UserId { get; set; } + + [Required] + public required Guid TrackId { get; set; } + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey(nameof(UserId))] + public User User { get; set; } + + [ForeignKey(nameof(TrackId))] + public Track Track { get; set; } + } +} diff --git a/backend/Core/Musify.Domain/Entities/UserFollow.cs b/backend/Core/Musify.Domain/Entities/UserFollow.cs new file mode 100644 index 0000000..1cd3322 --- /dev/null +++ b/backend/Core/Musify.Domain/Entities/UserFollow.cs @@ -0,0 +1,28 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace Musify.Domain.Entities +{ +#pragma warning disable CS8618 + [Table("UserFollows")] + public class UserFollow + { + [Key] + public Guid Id { get; set; } = Guid.NewGuid(); + + [Required] + public required long FollowerId { get; set; } + + [Required] + public required long FollowedId { get; set; } + + [Required] + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + + [ForeignKey(nameof(FollowerId))] + public User Follower { get; set; } + + [ForeignKey(nameof(FollowedId))] + public User Followed { get; set; } + } +} diff --git a/backend/Core/Musify.Domain/ValueObjects/PlaylistVisibility.cs b/backend/Core/Musify.Domain/ValueObjects/PlaylistVisibility.cs new file mode 100644 index 0000000..f809e24 --- /dev/null +++ b/backend/Core/Musify.Domain/ValueObjects/PlaylistVisibility.cs @@ -0,0 +1,8 @@ +namespace Musify.Domain.ValueObjects +{ + public enum PlaylistVisibility + { + Private = 0, + Public = 1 + } +} diff --git a/backend/Core/Musify.Infrastructure/Assets/PlayListDefaultCover.png b/backend/Core/Musify.Infrastructure/Assets/PlayListDefaultCover.png index 323650cbf986de79b11957015be0a2e3a865127c..0b7a38dba2e478d4a794ecc5bb7233ab3f0eebeb 100644 GIT binary patch literal 70143 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 literal 10489 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 VBeFu`T0u+b{da@63Ezo6^ artist.ExternalId) .IsUnique(); + modelBuilder.Entity() + .Property(playList => playList.Visibility) + .HasDefaultValue(PlaylistVisibility.Private); + modelBuilder.Entity() .OwnsOne(playList => playList.Pictures, pictures => { @@ -159,6 +164,26 @@ namespace Musify.Infrastructure.Persistence modelBuilder.Entity() .HasIndex(item => new { item.MixId, item.Position }); + modelBuilder.Entity() + .HasIndex(like => new { like.UserId, like.TrackId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(follow => follow.Follower) + .WithMany() + .HasForeignKey(follow => follow.FollowerId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(follow => follow.Followed) + .WithMany() + .HasForeignKey(follow => follow.FollowedId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(follow => new { follow.FollowerId, follow.FollowedId }) + .IsUnique(); + var playListProcessing = modelBuilder.Entity(); playListProcessing.HasKey(state => state.CorrelationId); playListProcessing.Property(state => state.CorrelationId).ValueGeneratedNever(); @@ -206,6 +231,10 @@ namespace Musify.Infrastructure.Persistence public DbSet ListeningHistories => Set(); + public DbSet TrackLikes => Set(); + + public DbSet UserFollows => Set(); + public async Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) { var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.Designer.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.Designer.cs new file mode 100644 index 0000000..2082406 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.Designer.cs @@ -0,0 +1,1245 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260914144913_AddPlayListVisibility")] + partial class AddPlayListVisibility + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Albums"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AlbumId") + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("TrackNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("AlbumId", "TrackId") + .IsUnique(); + + b.ToTable("AlbumHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalId") + .IsUnique(); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("ListeningHistory"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Position"); + + b.ToTable("Mixes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Artist") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("IsExplicit") + .HasColumnType("boolean"); + + b.Property("MixId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("VideoId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("MixId", "Position"); + + b.ToTable("MixItems"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.HasKey("TrackId", "ArtistId"); + + b.HasIndex("ArtistId"); + + b.ToTable("TrackArtist"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("AlbumProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("UserAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalTracks", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("LocalTracks", (string)null); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 => + { + b1.Property("AlbumId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("AlbumId"); + + b1.ToTable("Albums"); + + b1.WithOwner() + .HasForeignKey("AlbumId"); + }); + + b.Navigation("Pictures"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Album", "Album") + .WithMany("AlbumTracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("AlbumTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("ListeningHistories") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("ListeningHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.HasOne("Musify.Domain.Entities.Mix", "Mix") + .WithMany("Items") + .HasForeignKey("MixId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Mix"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("DownloadRequested") + .HasColumnType("boolean") + .HasColumnName("DownloadRequested"); + + b1.Property("FolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("AudioFolderName"); + + b1.Property("LastRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastRetryAt"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalAudioName"); + + b1.Property("RetryCount") + .HasColumnType("integer") + .HasColumnName("RetryCount"); + + b1.Property("TranscodeStatus") + .HasColumnType("integer") + .HasColumnName("AudioTranscodeProcessingStatus"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackPictures", "Pictures", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("ProcessingStatus") + .HasColumnType("integer") + .HasColumnName("PicturesProcessingStatus"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.Navigation("Audio") + .IsRequired(); + + b.Navigation("Pictures") + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.HasOne("Musify.Domain.Entities.Artist", "Artist") + .WithMany("TrackArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.ExternalTrack", "Track") + .WithMany("TrackArtists") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.UserAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.LocalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Navigation("AlbumTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("AlbumTracks"); + + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.Navigation("TrackArtists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.cs new file mode 100644 index 0000000..8e2bc87 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914144913_AddPlayListVisibility.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddPlayListVisibility : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Visibility", + table: "PlayLists", + type: "integer", + nullable: false, + defaultValue: 0); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Visibility", + table: "PlayLists"); + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.Designer.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.Designer.cs new file mode 100644 index 0000000..40e0fdf --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.Designer.cs @@ -0,0 +1,1289 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260914150045_AddTrackLikes")] + partial class AddTrackLikes + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Albums"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AlbumId") + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("TrackNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("AlbumId", "TrackId") + .IsUnique(); + + b.ToTable("AlbumHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalId") + .IsUnique(); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("ListeningHistory"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Position"); + + b.ToTable("Mixes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Artist") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("IsExplicit") + .HasColumnType("boolean"); + + b.Property("MixId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("VideoId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("MixId", "Position"); + + b.ToTable("MixItems"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.HasKey("TrackId", "ArtistId"); + + b.HasIndex("ArtistId"); + + b.ToTable("TrackArtist"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId", "TrackId") + .IsUnique(); + + b.ToTable("TrackLikes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("AlbumProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("UserAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalTracks", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("LocalTracks", (string)null); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 => + { + b1.Property("AlbumId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("AlbumId"); + + b1.ToTable("Albums"); + + b1.WithOwner() + .HasForeignKey("AlbumId"); + }); + + b.Navigation("Pictures"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Album", "Album") + .WithMany("AlbumTracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("AlbumTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("ListeningHistories") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("ListeningHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.HasOne("Musify.Domain.Entities.Mix", "Mix") + .WithMany("Items") + .HasForeignKey("MixId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Mix"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("DownloadRequested") + .HasColumnType("boolean") + .HasColumnName("DownloadRequested"); + + b1.Property("FolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("AudioFolderName"); + + b1.Property("LastRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastRetryAt"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalAudioName"); + + b1.Property("RetryCount") + .HasColumnType("integer") + .HasColumnName("RetryCount"); + + b1.Property("TranscodeStatus") + .HasColumnType("integer") + .HasColumnName("AudioTranscodeProcessingStatus"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackPictures", "Pictures", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("ProcessingStatus") + .HasColumnType("integer") + .HasColumnName("PicturesProcessingStatus"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.Navigation("Audio") + .IsRequired(); + + b.Navigation("Pictures") + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.HasOne("Musify.Domain.Entities.Artist", "Artist") + .WithMany("TrackArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.ExternalTrack", "Track") + .WithMany("TrackArtists") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.UserAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.LocalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Navigation("AlbumTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("AlbumTracks"); + + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.Navigation("TrackArtists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.cs new file mode 100644 index 0000000..609464e --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914150045_AddTrackLikes.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddTrackLikes : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TrackLikes", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "bigint", nullable: false), + TrackId = table.Column(type: "uuid", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TrackLikes", x => x.Id); + table.ForeignKey( + name: "FK_TrackLikes_Tracks_TrackId", + column: x => x.TrackId, + principalTable: "Tracks", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TrackLikes_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_TrackLikes_TrackId", + table: "TrackLikes", + column: "TrackId"); + + migrationBuilder.CreateIndex( + name: "IX_TrackLikes_UserId_TrackId", + table: "TrackLikes", + columns: new[] { "UserId", "TrackId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TrackLikes"); + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.Designer.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.Designer.cs new file mode 100644 index 0000000..9db303d --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.Designer.cs @@ -0,0 +1,1333 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Musify.Infrastructure.Persistence; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(Database))] + [Migration("20260914151900_AddUserFollows")] + partial class AddUserFollows + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.InboxState", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Consumed") + .HasColumnType("timestamp with time zone"); + + b.Property("ConsumerId") + .HasColumnType("uuid"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("ReceiveCount") + .HasColumnType("integer"); + + b.Property("Received") + .HasColumnType("timestamp with time zone"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Delivered"); + + b.ToTable("InboxState"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ReleaseYear") + .HasColumnType("integer"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Albums"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("AlbumId") + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("TrackNumber") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("AlbumId", "TrackId") + .IsUnique(); + + b.ToTable("AlbumHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalId") + .IsUnique(); + + b.ToTable("Artists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("ListenedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("ListeningHistory"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Subtitle") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId", "Position"); + + b.ToTable("Mixes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Artist") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("IsExplicit") + .HasColumnType("boolean"); + + b.Property("MixId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("Title") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("VideoId") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("MixId", "Position"); + + b.ToTable("MixItems"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("PlayLists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AddedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("PlayListId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("PlayListId"); + + b.HasIndex("TrackId"); + + b.ToTable("PlayListHasTrack"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("LifeCycleStatus") + .HasColumnType("integer"); + + b.Property("NormalizedTitle") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Tracks"); + + b.UseTptMappingStrategy(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("ArtistId") + .HasColumnType("uuid"); + + b.Property("Position") + .HasColumnType("integer"); + + b.HasKey("TrackId", "ArtistId"); + + b.HasIndex("ArtistId"); + + b.ToTable("TrackArtist"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId", "TrackId") + .IsUnique(); + + b.ToTable("TrackLikes"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreatedTime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("State") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.ToTable("Upload"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Bucket") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpectedSizeBytes") + .HasColumnType("bigint"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.Property("ObjectName") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Purpose") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("UploadIntents"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Property("Id") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FirstName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("NormalizedName") + .IsRequired() + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("ProfilePictureUrl") + .HasMaxLength(2048) + .HasColumnType("character varying(2048)"); + + b.Property("SecondName") + .HasMaxLength(48) + .HasColumnType("character varying(48)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FollowedId") + .HasColumnType("bigint"); + + b.Property("FollowerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FollowedId"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique(); + + b.ToTable("UserFollows"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId"); + + b.ToTable("UserHasTrack"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.AlbumProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("AlbumProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.PlayListProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("PlayListProcessingState"); + }); + + modelBuilder.Entity("Musify.Infrastructure.MassTransit.Sagas.TrackProcessingState", b => + { + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("AudioKey") + .HasColumnType("text"); + + b.Property("AudioProcessed") + .HasColumnType("boolean"); + + b.Property("Bucket") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CurrentState") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("PictureKey") + .HasColumnType("text"); + + b.Property("PictureProcessed") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("CorrelationId"); + + b.ToTable("TrackProcessingState"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.Property("ThumbnailUrl") + .HasMaxLength(512) + .HasColumnType("character varying(512)"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasBaseType("Musify.Domain.Entities.Album"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("UserAlbums", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("ExternalId") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("Source") + .HasColumnType("integer"); + + b.HasIndex("Source", "ExternalId") + .IsUnique(); + + b.ToTable("ExternalTracks", (string)null); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasBaseType("Musify.Domain.Entities.Track"); + + b.Property("OwnerUserId") + .HasColumnType("bigint"); + + b.HasIndex("OwnerUserId"); + + b.ToTable("LocalTracks", (string)null); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.OutboxState", null) + .WithMany() + .HasForeignKey("OutboxId"); + + b.HasOne("MassTransit.EntityFrameworkCoreIntegration.InboxState", null) + .WithMany() + .HasForeignKey("InboxMessageId", "InboxConsumerId") + .HasPrincipalKey("MessageId", "ConsumerId"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.AlbumPictures", "Pictures", b1 => + { + b1.Property("AlbumId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("AlbumId"); + + b1.ToTable("Albums"); + + b1.WithOwner() + .HasForeignKey("AlbumId"); + }); + + b.Navigation("Pictures"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.AlbumHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Album", "Album") + .WithMany("AlbumTracks") + .HasForeignKey("AlbumId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("AlbumTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Album"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ListeningHistory", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("ListeningHistories") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("ListeningHistories") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.MixItem", b => + { + b.HasOne("Musify.Domain.Entities.Mix", "Mix") + .WithMany("Items") + .HasForeignKey("MixId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Mix"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("PlayLists") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.OwnsOne("Musify.Domain.ValueObjects.PlayListPictures", "Pictures", b1 => + { + b1.Property("PlayListId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("SmallName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("PlayListId"); + + b1.ToTable("PlayLists"); + + b1.WithOwner() + .HasForeignKey("PlayListId"); + }); + + b.Navigation("Pictures"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayListHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.PlayList", "PlayList") + .WithMany("PlayListTracks") + .HasForeignKey("PlayListId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("PlayListTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlayList"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.OwnsOne("Musify.Domain.ValueObjects.TrackAudio", "Audio", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("DownloadRequested") + .HasColumnType("boolean") + .HasColumnName("DownloadRequested"); + + b1.Property("FolderName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("AudioFolderName"); + + b1.Property("LastRetryAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("LastRetryAt"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalAudioName"); + + b1.Property("RetryCount") + .HasColumnType("integer") + .HasColumnName("RetryCount"); + + b1.Property("TranscodeStatus") + .HasColumnType("integer") + .HasColumnName("AudioTranscodeProcessingStatus"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.OwnsOne("Musify.Domain.ValueObjects.TrackPictures", "Pictures", b1 => + { + b1.Property("TrackId") + .HasColumnType("uuid"); + + b1.Property("LargeName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("LargePictureName"); + + b1.Property("MediumName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("MediumPictureName"); + + b1.Property("OriginalName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("OriginalPictureName"); + + b1.Property("ProcessingStatus") + .HasColumnType("integer") + .HasColumnName("PicturesProcessingStatus"); + + b1.Property("SmallName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasColumnName("SmallPictureName"); + + b1.HasKey("TrackId"); + + b1.ToTable("Tracks"); + + b1.WithOwner() + .HasForeignKey("TrackId"); + }); + + b.Navigation("Audio") + .IsRequired(); + + b.Navigation("Pictures") + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackArtist", b => + { + b.HasOne("Musify.Domain.Entities.Artist", "Artist") + .WithMany("TrackArtists") + .HasForeignKey("ArtistId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.ExternalTrack", "Track") + .WithMany("TrackArtists") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Artist"); + + b.Navigation("Track"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => + { + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b => + { + b.HasOne("Musify.Domain.Entities.User", "Followed") + .WithMany() + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Follower") + .WithMany() + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany("UserTracks") + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany("UserTracks") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.UserAlbum", b => + { + b.HasOne("Musify.Domain.Entities.Album", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.UserAlbum", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.ExternalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Musify.Domain.Entities.LocalTrack", b => + { + b.HasOne("Musify.Domain.Entities.Track", null) + .WithOne() + .HasForeignKey("Musify.Domain.Entities.LocalTrack", "Id") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Owner") + .WithMany() + .HasForeignKey("OwnerUserId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Album", b => + { + b.Navigation("AlbumTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Artist", b => + { + b.Navigation("TrackArtists"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Mix", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.PlayList", b => + { + b.Navigation("PlayListTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.Track", b => + { + b.Navigation("AlbumTracks"); + + b.Navigation("ListeningHistories"); + + b.Navigation("PlayListTracks"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.User", b => + { + b.Navigation("ListeningHistories"); + + b.Navigation("PlayLists"); + + b.Navigation("UserTracks"); + }); + + modelBuilder.Entity("Musify.Domain.Entities.ExternalTrack", b => + { + b.Navigation("TrackArtists"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.cs new file mode 100644 index 0000000..afbf5f3 --- /dev/null +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/20260914151900_AddUserFollows.cs @@ -0,0 +1,59 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Musify.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddUserFollows : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "UserFollows", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FollowerId = table.Column(type: "bigint", nullable: false), + FollowedId = table.Column(type: "bigint", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserFollows", x => x.Id); + table.ForeignKey( + name: "FK_UserFollows_User_FollowedId", + column: x => x.FollowedId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_UserFollows_User_FollowerId", + column: x => x.FollowerId, + principalTable: "User", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_UserFollows_FollowedId", + table: "UserFollows", + column: "FollowedId"); + + migrationBuilder.CreateIndex( + name: "IX_UserFollows_FollowerId_FollowedId", + table: "UserFollows", + columns: new[] { "FollowerId", "FollowedId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "UserFollows"); + } + } +} diff --git a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs index 74000bb..001debd 100644 --- a/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs +++ b/backend/Core/Musify.Infrastructure/Persistence/Migrations/DatabaseModelSnapshot.cs @@ -431,6 +431,11 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Property("UserId") .HasColumnType("bigint"); + b.Property("Visibility") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0); + b.HasKey("Id"); b.HasIndex("UserId"); @@ -518,6 +523,31 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("TrackArtist"); }); + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("TrackId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("TrackId"); + + b.HasIndex("UserId", "TrackId") + .IsUnique(); + + b.ToTable("TrackLikes"); + }); + modelBuilder.Entity("Musify.Domain.Entities.Upload", b => { b.Property("Id") @@ -636,6 +666,31 @@ namespace Musify.Infrastructure.Persistence.Migrations b.ToTable("User"); }); + modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FollowedId") + .HasColumnType("bigint"); + + b.Property("FollowerId") + .HasColumnType("bigint"); + + b.HasKey("Id"); + + b.HasIndex("FollowedId"); + + b.HasIndex("FollowerId", "FollowedId") + .IsUnique(); + + b.ToTable("UserFollows"); + }); + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => { b.Property("Id") @@ -1105,6 +1160,25 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("Track"); }); + modelBuilder.Entity("Musify.Domain.Entities.TrackLike", b => + { + b.HasOne("Musify.Domain.Entities.Track", "Track") + .WithMany() + .HasForeignKey("TrackId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Track"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Musify.Domain.Entities.UploadIntent", b => { b.HasOne("Musify.Domain.Entities.User", "User") @@ -1116,6 +1190,25 @@ namespace Musify.Infrastructure.Persistence.Migrations b.Navigation("User"); }); + modelBuilder.Entity("Musify.Domain.Entities.UserFollow", b => + { + b.HasOne("Musify.Domain.Entities.User", "Followed") + .WithMany() + .HasForeignKey("FollowedId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Musify.Domain.Entities.User", "Follower") + .WithMany() + .HasForeignKey("FollowerId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Followed"); + + b.Navigation("Follower"); + }); + modelBuilder.Entity("Musify.Domain.Entities.UserHasTrack", b => { b.HasOne("Musify.Domain.Entities.Track", "Track") diff --git a/backend/Core/Musify.Infrastructure/Services/YouTubeMusicService.cs b/backend/Core/Musify.Infrastructure/Services/YouTubeMusicService.cs index 391875e..3444a7f 100644 --- a/backend/Core/Musify.Infrastructure/Services/YouTubeMusicService.cs +++ b/backend/Core/Musify.Infrastructure/Services/YouTubeMusicService.cs @@ -13,8 +13,8 @@ namespace Musify.Infrastructure.Services { public class YouTubeMusicService(YouTubeConfiguration configuration, IMemoryCache cache, ILogger logger) : IYouTubeMusicService { - private readonly YouTubeMusicClient client = new(geographicalLocation: configuration.GeographicalLocation); - private readonly SingleFlightCache singleFlight = new(cache); + private readonly YouTubeMusicClient client = new YouTubeMusicClient(geographicalLocation: configuration.GeographicalLocation); + private readonly SingleFlightCache singleFlight = new SingleFlightCache(cache); private sealed record CachedStreamInfo(string Url, DateTime ExpiresAtUtc); @@ -89,7 +89,7 @@ namespace Musify.Infrastructure.Services song.IsExplicit, song.Artists .Select(artist => new YouTubeArtistRef( - string.IsNullOrEmpty(artist.Id) ? null : artist.Id, + string.IsNullOrEmpty(artist.Id)? null: artist.Id, artist.Name)) .ToList())) .ToList(); @@ -147,7 +147,7 @@ namespace Musify.Infrastructure.Services info.IsEp, info.Artists .Select(artist => new YouTubeArtistRef( - string.IsNullOrEmpty(artist.Id) ? null : artist.Id, + string.IsNullOrEmpty(artist.Id)? null: artist.Id, artist.Name)) .ToList()); @@ -203,7 +203,7 @@ namespace Musify.Infrastructure.Services { var now = DateTime.UtcNow; if (cache.TryGetValue(StreamCacheKey(videoId), out CachedStreamInfo? cached) - && cached is not null + && cached != null && cached.ExpiresAtUtc > now) { return new YouTubeStreamInfo(cached.Url, (int)(cached.ExpiresAtUtc - now).TotalSeconds); @@ -267,7 +267,7 @@ namespace Musify.Infrastructure.Services info.IsExplicit, info.Artists .Select(artist => new YouTubeArtistRef( - string.IsNullOrEmpty(artist.Id) ? null : artist.Id, + string.IsNullOrEmpty(artist.Id)? null: artist.Id, artist.Name)) .ToList()); } @@ -291,8 +291,7 @@ namespace Musify.Infrastructure.Services var chosen = candidates.FirstOrDefault(thumbnail => thumbnail.Width >= configuration.ThumbnailSize) ?? candidates.LastOrDefault(); - return chosen is null - ? string.Empty + return chosen ==null? string.Empty : YouTubeThumbNailHelper.WithSize(chosen.Url, configuration.ThumbnailSize); } diff --git a/backend/Hosts/Musify.Api/Authentication/AuthenticationExtensions.cs b/backend/Hosts/Musify.Api/Authentication/AuthenticationExtensions.cs index 2f8416b..45a31b9 100644 --- a/backend/Hosts/Musify.Api/Authentication/AuthenticationExtensions.cs +++ b/backend/Hosts/Musify.Api/Authentication/AuthenticationExtensions.cs @@ -18,6 +18,7 @@ public static class AuthenticationExtensions serviceProvider.GetRequiredService>().Value); services.AddMemoryCache(); + services.AddHttpClient(); services.AddScoped(); services.ConfigureOptions(); diff --git a/backend/Hosts/Musify.Api/Authentication/JwtBearerEventsHandler.cs b/backend/Hosts/Musify.Api/Authentication/JwtBearerEventsHandler.cs index 02c091c..7f1b7e3 100644 --- a/backend/Hosts/Musify.Api/Authentication/JwtBearerEventsHandler.cs +++ b/backend/Hosts/Musify.Api/Authentication/JwtBearerEventsHandler.cs @@ -2,17 +2,30 @@ using Mediator; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.Extensions.Caching.Memory; using Musify.Application.Users; +using System.Net.Http.Headers; +using System.Net.Http.Json; using System.Security.Claims; +using System.Text.Json.Serialization; namespace Musify.Api.Authentication; public sealed class JwtBearerEventsHandler( IMediator mediator, IMemoryCache cache, + IHttpClientFactory httpClientFactory, + AuthenticationConfiguration authenticationConfiguration, ILogger logger) : JwtBearerEvents { private static readonly TimeSpan UserSyncCacheTtl = TimeSpan.FromMinutes(15); + private sealed record UserInfoResponse( + [property: JsonPropertyName("name")] string? Name, + [property: JsonPropertyName("preferred_username")] string? PreferredUsername, + [property: JsonPropertyName("email")] string? Email, + [property: JsonPropertyName("given_name")] string? GivenName, + [property: JsonPropertyName("family_name")] string? FamilyName, + [property: JsonPropertyName("picture")] string? Picture); + public override async Task TokenValidated(TokenValidatedContext tokenValidatedContext) { var principal = tokenValidatedContext.Principal!; @@ -28,14 +41,30 @@ public sealed class JwtBearerEventsHandler( if (cache.TryGetValue(cacheKey, out _)) return; - var username = principal.FindFirstValue(ClaimTypes.Name) + // 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") + ?? principal.FindFirstValue(ClaimTypes.Name) ?? principal.FindFirstValue("preferred_username") + ?? userInfo?.Email + ?? principal.FindFirstValue("email") ?? principal.FindFirstValue(ClaimTypes.Email) ?? rawId!; - var firstName = principal.FindFirstValue(ClaimTypes.GivenName); - var lastName = principal.FindFirstValue(ClaimTypes.Surname); - var profilePictureUrl = principal.FindFirstValue("picture"); + var firstName = userInfo?.GivenName + ?? principal.FindFirstValue("given_name") + ?? principal.FindFirstValue(ClaimTypes.GivenName); + var lastName = userInfo?.FamilyName + ?? principal.FindFirstValue("family_name") + ?? principal.FindFirstValue(ClaimTypes.Surname); + var profilePictureUrl = userInfo?.Picture ?? principal.FindFirstValue("picture"); try { @@ -51,6 +80,43 @@ public sealed class JwtBearerEventsHandler( } } + private async Task FetchUserInfoAsync( + TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken) + { + var authorizationHeader = tokenValidatedContext.HttpContext.Request.Headers.Authorization.ToString(); + if (!authorizationHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) + return null; + + var accessToken = authorizationHeader["Bearer ".Length..].Trim(); + if (accessToken.Length == 0) + return null; + + try + { + var client = httpClientFactory.CreateClient(); + var request = new HttpRequestMessage( + HttpMethod.Get, + $"{authenticationConfiguration.IssuerAddress.TrimEnd('/')}/oidc/v1/userinfo") + { + Headers = { Authorization = new AuthenticationHeaderValue("Bearer", accessToken) } + }; + + var response = await client.SendAsync(request, cancellationToken); + if (!response.IsSuccessStatusCode) + { + logger.LogDebug("Userinfo request returned {StatusCode}", response.StatusCode); + return null; + } + + return await response.Content.ReadFromJsonAsync(cancellationToken); + } + catch (Exception exception) + { + logger.LogWarning(exception, "Failed to fetch userinfo from identity provider"); + return null; + } + } + public override Task AuthenticationFailed(AuthenticationFailedContext context) { logger.LogWarning(context.Exception, "JWT authentication failed"); diff --git a/backend/Hosts/Musify.Api/DataTransferObjects/Likes/ToggleLikeRequest.cs b/backend/Hosts/Musify.Api/DataTransferObjects/Likes/ToggleLikeRequest.cs new file mode 100644 index 0000000..90f8f85 --- /dev/null +++ b/backend/Hosts/Musify.Api/DataTransferObjects/Likes/ToggleLikeRequest.cs @@ -0,0 +1,8 @@ +using Musify.Domain.ValueObjects; + +namespace Musify.Api.DataTransferObjects.Likes; + +/// Toggle a like on a track already in the catalog by , or one from an +/// external source by + (provisioning it in the +/// background if needed) — exactly one of the two must be given. +public record ToggleLikeRequest(Guid? TrackId, TrackSource? Source, string? ExternalId); diff --git a/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/CreatePlayListRequest.cs b/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/CreatePlayListRequest.cs index 47792c0..bc5910e 100644 --- a/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/CreatePlayListRequest.cs +++ b/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/CreatePlayListRequest.cs @@ -1,6 +1,9 @@ +using Musify.Domain.ValueObjects; + namespace Musify.Api.DataTransferObjects.PlayLists; public record CreatePlayListRequest( string Name, string? Description, - Guid? PictureIntentId); + Guid? PictureIntentId, + PlaylistVisibility Visibility = PlaylistVisibility.Private); diff --git a/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs b/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs index 5ac5700..0ccf8fc 100644 --- a/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs +++ b/backend/Hosts/Musify.Api/DataTransferObjects/PlayLists/UpdatePlayListRequest.cs @@ -1,6 +1,9 @@ +using Musify.Domain.ValueObjects; + namespace Musify.Api.DataTransferObjects.PlayLists; public record UpdatePlayListRequest( string? NewName, string? NewDescription, - Guid? NewPictureIntentId); + Guid? NewPictureIntentId, + PlaylistVisibility? NewVisibility = null); diff --git a/backend/Hosts/Musify.Api/Endpoints/LikeEndpoints.cs b/backend/Hosts/Musify.Api/Endpoints/LikeEndpoints.cs new file mode 100644 index 0000000..c707cd9 --- /dev/null +++ b/backend/Hosts/Musify.Api/Endpoints/LikeEndpoints.cs @@ -0,0 +1,61 @@ +using Mediator; +using Musify.Application.Likes; +using Musify.Application.Shared; +using Musify.Application.Tracks.Responses; +using Musify.Api.Authentication; +using Musify.Api.DataTransferObjects.Likes; +using Musify.Api.Extensions; +using Musify.Api.Filters; + +namespace Musify.Api.Endpoints; + +public static class LikeEndpoints +{ + public static IEndpointRouteBuilder MapLikeEndpoints(this IEndpointRouteBuilder app) + { + var group = app.MapGroup("/likes") + .WithTags("Likes") + .RequireAuthorization(); + + group.MapGet("/", GetLikedTracks) + .WithName("GetLikedTracks") + .WithSummary("Get The Current User'S Liked Tracks.") + .Produces>() + .Produces(StatusCodes.Status401Unauthorized); + + group.MapPost("/toggle", ToggleTrackLike) + .WithName("ToggleTrackLike") + .WithSummary("Like Or Unlike A Track, By Id Or By External Source, Provisioning It In The Background If Needed.") + .AddEndpointFilter>() + .Produces() + .ProducesValidationProblem() + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound); + + return app; + } + + private static async Task GetLikedTracks( + IMediator mediator, + CurrentUser currentUser, + CancellationToken cancellationToken, + int pageNumber = 1, + int pageSize = 10) + { + var result = await mediator.Send(new GetLikedTracksQuery(currentUser.RequiredId, pageNumber, pageSize), cancellationToken); + return result.ToHttpResult(); + } + + private static async Task ToggleTrackLike( + IMediator mediator, + CurrentUser currentUser, + ToggleLikeRequest request, + CancellationToken cancellationToken) + { + var result = await mediator.Send( + new ToggleTrackLikeCommand(currentUser.RequiredId, request.TrackId, request.Source, request.ExternalId), + cancellationToken); + + return result.ToHttpResult(); + } +} diff --git a/backend/Hosts/Musify.Api/Endpoints/PlayListEndpoints.cs b/backend/Hosts/Musify.Api/Endpoints/PlayListEndpoints.cs index a5d3c43..3004fbd 100644 --- a/backend/Hosts/Musify.Api/Endpoints/PlayListEndpoints.cs +++ b/backend/Hosts/Musify.Api/Endpoints/PlayListEndpoints.cs @@ -120,10 +120,11 @@ public static class PlayListEndpoints private static async Task GetPlayListById( IMediator mediator, + CurrentUser currentUser, Guid id, CancellationToken cancellationToken) { - var result = await mediator.Send(new GetPlayListByIdQuery(id), cancellationToken); + var result = await mediator.Send(new GetPlayListByIdQuery(id, currentUser.Id), cancellationToken); return result.ToHttpResult(); } @@ -133,9 +134,10 @@ public static class PlayListEndpoints CancellationToken cancellationToken, string? name, int pageNumber = 1, - int pageSize = 10) + int pageSize = 10, + bool onlyPublic = false) { - var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, pageNumber, pageSize), cancellationToken); + var result = await mediator.Send(new GetPlayListsByUserIdQuery(userId, name, pageNumber, pageSize, onlyPublic), cancellationToken); return result.ToHttpResult(); } @@ -175,7 +177,7 @@ public static class PlayListEndpoints CancellationToken cancellationToken) { var result = await mediator.Send( - new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId), + new CreatePlayListCommand(currentUser.RequiredId, request.Name, request.Description, request.PictureIntentId, request.Visibility), cancellationToken); return result.ToCreatedResult(playList => $"/playlists/{playList.Id}"); @@ -189,7 +191,7 @@ public static class PlayListEndpoints CancellationToken cancellationToken) { var result = await mediator.Send( - new UpdatePlayListCommand(currentUser.RequiredId, playlistId, request.NewName, request.NewDescription, request.NewPictureIntentId), + new UpdatePlayListCommand(currentUser.RequiredId, playlistId, request.NewName, request.NewDescription, request.NewPictureIntentId, request.NewVisibility), cancellationToken); return result.ToHttpResult(); diff --git a/backend/Hosts/Musify.Api/Endpoints/UserEndpoints.cs b/backend/Hosts/Musify.Api/Endpoints/UserEndpoints.cs index 00539d0..e2cc12c 100644 --- a/backend/Hosts/Musify.Api/Endpoints/UserEndpoints.cs +++ b/backend/Hosts/Musify.Api/Endpoints/UserEndpoints.cs @@ -1,4 +1,5 @@ using Mediator; +using Musify.Api.Authentication; using Musify.Api.DataTransferObjects.Users; using Musify.Api.Extensions; using Musify.Api.Filters; @@ -34,6 +35,40 @@ public static class UserEndpoints .Produces>() .Produces(StatusCodes.Status404NotFound); + group.MapGet("/{id}/listening-stats", GetListeningStats) + .WithName("GetListeningStats") + .WithSummary("Get A User'S Listening Stats For The Current Week And Streak.") + .Produces(); + + group.MapGet("/{id}/profile", GetUserProfile) + .WithName("GetUserProfile") + .WithSummary("Get A User'S Public Profile, Including Follower Counts.") + .Produces() + .Produces(StatusCodes.Status404NotFound); + + group.MapGet("/{id}/is-following", IsFollowing) + .WithName("IsFollowingUser") + .WithSummary("Check Whether The Current User Follows Another User.") + .RequireAuthorization() + .Produces() + .Produces(StatusCodes.Status401Unauthorized); + + group.MapPost("/{id}/follow", FollowUser) + .WithName("FollowUser") + .WithSummary("Follow A User.") + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent) + .Produces(StatusCodes.Status401Unauthorized) + .Produces(StatusCodes.Status404NotFound) + .ProducesValidationProblem(); + + group.MapDelete("/{id}/follow", UnfollowUser) + .WithName("UnfollowUser") + .WithSummary("Unfollow A User.") + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent) + .Produces(StatusCodes.Status401Unauthorized); + group.MapPost("/", CreateUser) .WithName("CreateUser") .WithSummary("Create A New User.") @@ -73,6 +108,52 @@ public static class UserEndpoints return await mediator.Send(new GetListeningHistoryQuery(id), cancellationToken); } + private static async Task GetListeningStats( + IMediator mediator, + long id, + CancellationToken cancellationToken) + { + return await mediator.Send(new GetListeningStatsQuery(id), cancellationToken); + } + + private static async Task GetUserProfile( + IMediator mediator, + long id, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new GetUserProfileQuery(id), cancellationToken); + return result.ToHttpResult(); + } + + private static async Task IsFollowing( + IMediator mediator, + CurrentUser currentUser, + long id, + CancellationToken cancellationToken) + { + return await mediator.Send(new IsFollowingUserQuery(currentUser.RequiredId, id), cancellationToken); + } + + private static async Task FollowUser( + IMediator mediator, + CurrentUser currentUser, + long id, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new FollowUserCommand(currentUser.RequiredId, id), cancellationToken); + return result.ToHttpResult(); + } + + private static async Task UnfollowUser( + IMediator mediator, + CurrentUser currentUser, + long id, + CancellationToken cancellationToken) + { + var result = await mediator.Send(new UnfollowUserCommand(currentUser.RequiredId, id), cancellationToken); + return result.ToHttpResult(); + } + private static async Task CreateUser( IMediator mediator, CreateUserRequest request, diff --git a/backend/Hosts/Musify.Api/OpenApi/OpenApiOptionsSetup.cs b/backend/Hosts/Musify.Api/OpenApi/OpenApiOptionsSetup.cs index 24e0bab..a6c75df 100644 --- a/backend/Hosts/Musify.Api/OpenApi/OpenApiOptionsSetup.cs +++ b/backend/Hosts/Musify.Api/OpenApi/OpenApiOptionsSetup.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.OpenApi; using Microsoft.Extensions.Options; using Microsoft.OpenApi; using Musify.Api.Authentication; +using Musify.Application.Serialization; namespace Musify.Api.OpenApi; @@ -38,6 +39,24 @@ public sealed class OpenApiOptionsSetup(IOptions 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) + { + schema.Type = JsonSchemaType.String; + schema.Format = null; + } + + return Task.CompletedTask; + } ); + options.AddOperationTransformer((operation, context, _) => { var requiresAuthorization = context.Description.ActionDescriptor.EndpointMetadata diff --git a/backend/Hosts/Musify.Api/Program.cs b/backend/Hosts/Musify.Api/Program.cs index 18e3e6d..524ed83 100644 --- a/backend/Hosts/Musify.Api/Program.cs +++ b/backend/Hosts/Musify.Api/Program.cs @@ -40,6 +40,7 @@ app.UseAuthorization(); app .MapAlbumEndpoints() .MapConfigEndpoints() + .MapLikeEndpoints() .MapMixEndpoints() .MapPlayListEndpoints() .MapTrackEndpoints() diff --git a/backend/Hosts/Musify.Api/Validators/Likes/ToggleLikeRequestValidator.cs b/backend/Hosts/Musify.Api/Validators/Likes/ToggleLikeRequestValidator.cs new file mode 100644 index 0000000..13fec0c --- /dev/null +++ b/backend/Hosts/Musify.Api/Validators/Likes/ToggleLikeRequestValidator.cs @@ -0,0 +1,27 @@ +using FluentValidation; +using Musify.Api.DataTransferObjects.Likes; +using Musify.Domain.ValueObjects; + +namespace Musify.Api.Validators.Likes; + +public sealed class ToggleLikeRequestValidator : AbstractValidator +{ + public ToggleLikeRequestValidator() + { + RuleFor(x => x) + .Must(x => x.TrackId.HasValue ^ (x.Source.HasValue && !string.IsNullOrEmpty(x.ExternalId))) + .WithMessage("Provide either trackId, or source and externalId, but not both."); + + When(x => x.Source.HasValue, () => + { + RuleFor(x => x.Source) + .Must(source => source == TrackSource.YouTube) + .WithMessage("Unsupported source."); + + RuleFor(x => x.ExternalId) + .NotEmpty() + .Matches("^[A-Za-z0-9_-]{11}$") + .WithMessage("externalId must be a valid YouTube video id."); + }); + } +} diff --git a/backend/Tests/Musify.Application.Tests/Likes/GetLikedTracksQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Likes/GetLikedTracksQueryHandlerTests.cs new file mode 100644 index 0000000..9be76bb --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Likes/GetLikedTracksQueryHandlerTests.cs @@ -0,0 +1,55 @@ +using Musify.Application.Likes; +using Musify.Application.Tests.TestSupport; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Likes +{ + public sealed class GetLikedTracksQueryHandlerTests : HandlerTestBase + { + private GetLikedTracksQueryHandler CreateHandler() => new(Database); + + [Fact] + public async Task Handle_MultipleLikes_ReturnsMostRecentFirst() + { + var owner = TestEntities.User(); + var older = TestEntities.LocalTrack(owner, "Older like"); + var newer = TestEntities.LocalTrack(owner, "Newer like"); + await SeedAsync( + owner, older, newer, + new TrackLike { UserId = owner.Id, TrackId = older.Id, CreatedAt = DateTime.UtcNow.AddMinutes(-10) }, + new TrackLike { UserId = owner.Id, TrackId = newer.Id, CreatedAt = DateTime.UtcNow }); + + var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(["Newer like", "Older like"], result.Value.Items.Select(track => track.Title)); + } + + [Fact] + public async Task Handle_NoLikes_ReturnsEmpty() + { + var owner = TestEntities.User(); + await SeedAsync(owner); + + var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Empty(result.Value.Items); + } + + [Fact] + public async Task Handle_OtherUsersLikes_AreExcluded() + { + var owner = TestEntities.User(1, "owner"); + var stranger = TestEntities.User(2, "stranger"); + var track = TestEntities.LocalTrack(owner); + await SeedAsync(owner, stranger, track, new TrackLike { UserId = stranger.Id, TrackId = track.Id }); + + var result = await CreateHandler().Handle(new GetLikedTracksQuery(owner.Id, 1, 10), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Empty(result.Value.Items); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/Likes/ToggleTrackLikeCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Likes/ToggleTrackLikeCommandHandlerTests.cs new file mode 100644 index 0000000..a0ba867 --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Likes/ToggleTrackLikeCommandHandlerTests.cs @@ -0,0 +1,103 @@ +using ErrorOr; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Contracts; +using Musify.Application.Likes; +using Musify.Application.Services; +using Musify.Application.Tests.TestSupport; +using Musify.Domain.ValueObjects; +using NSubstitute; +using Xunit; + +namespace Musify.Application.Tests.Likes +{ + public sealed class ToggleTrackLikeCommandHandlerTests : HandlerTestBase + { + private readonly IYouTubeMusicService youTubeMusicService = Substitute.For(); + + private ToggleTrackLikeCommandHandler CreateHandler() + { + var provisioner = new YouTubeTrackProvisioner( + Database, youTubeMusicService, TestConfigurations.Track(), NoOpLogger()); + + return new ToggleTrackLikeCommandHandler(Database, provisioner, NoOpLogger()); + } + + private static YouTubeSongResult BuildSong(string videoId) => new( + videoId, "Song Title", "Song Artist", "Song Album", 200, "https://img.example/thumb.jpg", false, + [new YouTubeArtistRef("artist-id", "Song Artist")]); + + [Fact] + public async Task Handle_LocalTrackNotLiked_LikesIt() + { + var owner = TestEntities.User(); + var track = TestEntities.LocalTrack(owner); + await SeedAsync(owner, track); + + var result = await CreateHandler().Handle( + new ToggleTrackLikeCommand(owner.Id, track.Id, null, null), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.True(result.Value); + Assert.Single(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_LocalTrackAlreadyLiked_UnlikesIt() + { + var owner = TestEntities.User(); + var track = TestEntities.LocalTrack(owner); + await SeedAsync(owner, track, new Musify.Domain.Entities.TrackLike { UserId = owner.Id, TrackId = track.Id }); + + var result = await CreateHandler().Handle( + new ToggleTrackLikeCommand(owner.Id, track.Id, null, null), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.False(result.Value); + Assert.Empty(await Database.TrackLikes.ToListAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_TrackMissing_ReturnsNotFound() + { + var owner = TestEntities.User(); + await SeedAsync(owner); + + var result = await CreateHandler().Handle( + new ToggleTrackLikeCommand(owner.Id, Guid.NewGuid(), null, null), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.NotFound, result.FirstError.Type); + } + + [Fact] + public async Task Handle_UnknownExternalTrack_ProvisionsAndLikesIt() + { + youTubeMusicService.GetSongAsync("new-video", Arg.Any()).Returns(BuildSong("new-video")); + + var owner = TestEntities.User(); + await SeedAsync(owner); + + var result = await CreateHandler().Handle( + new ToggleTrackLikeCommand(owner.Id, null, TrackSource.YouTube, "new-video"), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.True(result.Value); + var track = await Database.ExternalTracks.SingleAsync(t => t.ExternalId == "new-video", TestContext.Current.CancellationToken); + Assert.True(await Database.TrackLikes.AnyAsync(like => like.TrackId == track.Id, TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_ExistingExternalTrack_LikesItWithoutReprovisioning() + { + var owner = TestEntities.User(); + var track = TestEntities.ExternalTrack(externalId: "abc123"); + await SeedAsync(owner, track); + + var result = await CreateHandler().Handle( + new ToggleTrackLikeCommand(owner.Id, null, TrackSource.YouTube, "abc123"), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.True(result.Value); + await youTubeMusicService.DidNotReceive().GetSongAsync(Arg.Any(), Arg.Any()); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/PlayLists/CreatePlayListCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/PlayLists/CreatePlayListCommandHandlerTests.cs index ee7035f..c7c080d 100644 --- a/backend/Tests/Musify.Application.Tests/PlayLists/CreatePlayListCommandHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/PlayLists/CreatePlayListCommandHandlerTests.cs @@ -60,6 +60,34 @@ namespace Musify.Application.Tests.PlayLists await eventBus.Received(1).PublishAsync(Arg.Any(), Arg.Any()); } + [Fact] + public async Task Handle_NoVisibilitySpecified_DefaultsToPrivate() + { + var user = TestEntities.User(); + await SeedAsync(user); + + var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null); + + var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Private, result.Value.Visibility); + } + + [Fact] + public async Task Handle_PublicVisibility_PersistsIt() + { + var user = TestEntities.User(); + await SeedAsync(user); + + var command = new CreatePlayListCommand(user.Id, "My Playlist", null, PictureIntentId: null, Visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public); + + var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility); + } + [Fact] public async Task Handle_UserMissing_ReturnsNotFound() { diff --git a/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListByIdQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListByIdQueryHandlerTests.cs index 5a68047..45c5483 100644 --- a/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListByIdQueryHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListByIdQueryHandlerTests.cs @@ -14,7 +14,7 @@ namespace Musify.Application.Tests.PlayLists public async Task Handle_ExistingPlayList_ReturnsItWithCoverTrackIds() { var owner = TestEntities.User(); - var playList = TestEntities.PlayList(owner.Id); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public); var track = TestEntities.LocalTrack(owner); await SeedAsync(owner, playList, track, new PlayListHasTrack { PlayListId = playList.Id, TrackId = track.Id, Position = 0 }); @@ -32,5 +32,54 @@ namespace Musify.Application.Tests.PlayLists Assert.Equal(ErrorType.NotFound, result.FirstError.Type); } + + [Fact] + public async Task Handle_PrivatePlayList_RequestedByOwner_ReturnsIt() + { + var owner = TestEntities.User(); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private); + await SeedAsync(owner, playList); + + var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, owner.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + } + + [Fact] + public async Task Handle_PrivatePlayList_RequestedByStranger_ReturnsNotFound() + { + var owner = TestEntities.User(1, "owner"); + var stranger = TestEntities.User(2, "stranger"); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private); + await SeedAsync(owner, stranger, playList); + + var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, stranger.Id), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.NotFound, result.FirstError.Type); + } + + [Fact] + public async Task Handle_PrivatePlayList_RequestedAnonymously_ReturnsNotFound() + { + var owner = TestEntities.User(); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Private); + await SeedAsync(owner, playList); + + var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.NotFound, result.FirstError.Type); + } + + [Fact] + public async Task Handle_PublicPlayList_RequestedAnonymously_ReturnsIt() + { + var owner = TestEntities.User(); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public); + await SeedAsync(owner, playList); + + var result = await CreateHandler().Handle(new GetPlayListByIdQuery(playList.Id, RequestingUserId: null), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + } } } diff --git a/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListsByUserIdQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListsByUserIdQueryHandlerTests.cs index 6a3bd17..4e051e5 100644 --- a/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListsByUserIdQueryHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/PlayLists/GetPlayListsByUserIdQueryHandlerTests.cs @@ -27,6 +27,22 @@ namespace Musify.Application.Tests.PlayLists Assert.Equal("Mine", playList.Name); } + [Fact] + public async Task Handle_OnlyPublic_ReturnsOnlyPublicPlayLists() + { + var owner = TestEntities.User(); + await SeedAsync( + owner, + TestEntities.PlayList(owner.Id, "Public one", visibility: PlaylistVisibility.Public), + TestEntities.PlayList(owner.Id, "Private one", visibility: PlaylistVisibility.Private)); + + var result = await CreateHandler().Handle(new GetPlayListsByUserIdQuery(owner.Id, Name: null, PageNumber: 1, PageSize: 10, OnlyPublic: true), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + var playList = Assert.Single(result.Value.Items); + Assert.Equal("Public one", playList.Name); + } + [Fact] public async Task Handle_NameFilter_ReturnsOnlyMatchingPlayLists() { diff --git a/backend/Tests/Musify.Application.Tests/PlayLists/UpdatePlayListCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/PlayLists/UpdatePlayListCommandHandlerTests.cs index 683ed23..10e1c20 100644 --- a/backend/Tests/Musify.Application.Tests/PlayLists/UpdatePlayListCommandHandlerTests.cs +++ b/backend/Tests/Musify.Application.Tests/PlayLists/UpdatePlayListCommandHandlerTests.cs @@ -62,6 +62,36 @@ namespace Musify.Application.Tests.PlayLists Assert.Equal(ErrorType.Unauthorized, result.FirstError.Type); } + [Fact] + public async Task Handle_NewVisibility_UpdatesIt() + { + var owner = TestEntities.User(); + var playList = TestEntities.PlayList(owner.Id); + await SeedAsync(owner, playList); + + var command = new UpdatePlayListCommand(owner.Id, playList.Id, null, null, null, Musify.Domain.ValueObjects.PlaylistVisibility.Public); + + var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility); + } + + [Fact] + public async Task Handle_NoVisibilitySpecified_LeavesItUnchanged() + { + var owner = TestEntities.User(); + var playList = TestEntities.PlayList(owner.Id, visibility: Musify.Domain.ValueObjects.PlaylistVisibility.Public); + await SeedAsync(owner, playList); + + var command = new UpdatePlayListCommand(owner.Id, playList.Id, "New Name", null, null); + + var result = await CreateHandler().Handle(command, TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(Musify.Domain.ValueObjects.PlaylistVisibility.Public, result.Value.Visibility); + } + [Fact] public async Task Handle_BlankNameAndDescription_LeavesThemUnchanged() { diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs index 80a240b..a689c5f 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestDatabase.cs @@ -126,6 +126,26 @@ namespace Musify.Application.Tests.TestSupport modelBuilder.Entity() .HasIndex(item => new { item.MixId, item.Position }); + + modelBuilder.Entity() + .HasIndex(like => new { like.UserId, like.TrackId }) + .IsUnique(); + + modelBuilder.Entity() + .HasOne(follow => follow.Follower) + .WithMany() + .HasForeignKey(follow => follow.FollowerId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasOne(follow => follow.Followed) + .WithMany() + .HasForeignKey(follow => follow.FollowedId) + .OnDelete(DeleteBehavior.Restrict); + + modelBuilder.Entity() + .HasIndex(follow => new { follow.FollowerId, follow.FollowedId }) + .IsUnique(); } public DbSet Users => Set(); @@ -164,6 +184,10 @@ namespace Musify.Application.Tests.TestSupport public DbSet ListeningHistories => Set(); + public DbSet TrackLikes => Set(); + + public DbSet UserFollows => Set(); + public async Task BeginTransactionAsync(System.Data.IsolationLevel isolationLevel, CancellationToken cancellationToken) { var transaction = await Database.BeginTransactionAsync(isolationLevel, cancellationToken); diff --git a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs index 5cbeaa9..4e16da1 100644 --- a/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs +++ b/backend/Tests/Musify.Application.Tests/TestSupport/TestEntities.cs @@ -111,7 +111,8 @@ namespace Musify.Application.Tests.TestSupport string name = "Test Playlist", string? description = null, PlayListPictures? pictures = null, - LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active) => + LifeCycleStatus lifeCycleStatus = LifeCycleStatus.Active, + PlaylistVisibility visibility = PlaylistVisibility.Private) => new() { UserId = userId, @@ -119,7 +120,8 @@ namespace Musify.Application.Tests.TestSupport NormalizedName = name.ToUpperInvariant(), Description = description, Pictures = pictures ?? PlayListPictures(), - LifeCycleStatus = lifeCycleStatus + LifeCycleStatus = lifeCycleStatus, + Visibility = visibility }; public static AlbumPictures AlbumPictures() => new() diff --git a/backend/Tests/Musify.Application.Tests/Users/FollowUserCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Users/FollowUserCommandHandlerTests.cs new file mode 100644 index 0000000..3659bbf --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Users/FollowUserCommandHandlerTests.cs @@ -0,0 +1,61 @@ +using ErrorOr; +using Microsoft.EntityFrameworkCore; +using Musify.Application.Tests.TestSupport; +using Musify.Application.Users; +using Xunit; + +namespace Musify.Application.Tests.Users +{ + public sealed class FollowUserCommandHandlerTests : HandlerTestBase + { + private FollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger()); + + [Fact] + public async Task Handle_ValidTarget_CreatesFollow() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed); + + var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_AlreadyFollowing_IsIdempotent() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed, new Musify.Domain.Entities.UserFollow { FollowerId = follower.Id, FollowedId = followed.Id }); + + var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Single(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_SelfFollow_ReturnsValidationError() + { + var user = TestEntities.User(); + await SeedAsync(user); + + var result = await CreateHandler().Handle(new FollowUserCommand(user.Id, user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.Validation, result.FirstError.Type); + } + + [Fact] + public async Task Handle_TargetMissing_ReturnsNotFound() + { + var follower = TestEntities.User(); + await SeedAsync(follower); + + var result = await CreateHandler().Handle(new FollowUserCommand(follower.Id, 404), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.NotFound, result.FirstError.Type); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/Users/GetListeningStatsQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Users/GetListeningStatsQueryHandlerTests.cs new file mode 100644 index 0000000..bd3a725 --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Users/GetListeningStatsQueryHandlerTests.cs @@ -0,0 +1,87 @@ +using Musify.Application.Tests.TestSupport; +using Musify.Application.Users; +using Xunit; + +namespace Musify.Application.Tests.Users +{ + public sealed class GetListeningStatsQueryHandlerTests : HandlerTestBase + { + private GetListeningStatsQueryHandler CreateHandler() => new(Database); + + [Fact] + public async Task Handle_NoHistory_ReturnsZeroes() + { + var user = TestEntities.User(); + await SeedAsync(user); + + var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(0, result.TracksThisWeek); + Assert.Equal(0, result.SecondsThisWeek); + Assert.Equal(0, result.StreakDays); + } + + [Fact] + public async Task Handle_ListensThisWeek_CountsDistinctTracksAndTotalSeconds() + { + var user = TestEntities.User(); + var trackA = TestEntities.LocalTrack(user, "Track A", durationSeconds: 200); + var trackB = TestEntities.LocalTrack(user, "Track B", durationSeconds: 100); + await SeedAsync( + user, trackA, trackB, + TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddDays(-1)), + TestEntities.ListeningHistory(user.Id, trackA.Id, DateTime.UtcNow.AddHours(-1)), + TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow), + TestEntities.ListeningHistory(user.Id, trackB.Id, DateTime.UtcNow.AddDays(-10))); + + var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(2, result.TracksThisWeek); + Assert.Equal(500, result.SecondsThisWeek); + } + + [Fact] + public async Task Handle_ListenedTodayAndYesterday_StreakIsTwo() + { + var user = TestEntities.User(); + var track = TestEntities.LocalTrack(user); + await SeedAsync( + user, track, + TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow), + TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1))); + + var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(2, result.StreakDays); + } + + [Fact] + public async Task Handle_GapBeforeToday_StopsStreakAtGap() + { + var user = TestEntities.User(); + var track = TestEntities.LocalTrack(user); + await SeedAsync( + user, track, + TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow), + TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-2))); + + var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(1, result.StreakDays); + } + + [Fact] + public async Task Handle_NoListenToday_StreakIsZero() + { + var user = TestEntities.User(); + var track = TestEntities.LocalTrack(user); + await SeedAsync( + user, track, + TestEntities.ListeningHistory(user.Id, track.Id, DateTime.UtcNow.AddDays(-1))); + + var result = await CreateHandler().Handle(new GetListeningStatsQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.Equal(0, result.StreakDays); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/Users/GetUserProfileQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Users/GetUserProfileQueryHandlerTests.cs new file mode 100644 index 0000000..57b734e --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Users/GetUserProfileQueryHandlerTests.cs @@ -0,0 +1,41 @@ +using ErrorOr; +using Musify.Application.Tests.TestSupport; +using Musify.Application.Users; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Users +{ + public sealed class GetUserProfileQueryHandlerTests : HandlerTestBase + { + private GetUserProfileQueryHandler CreateHandler() => new(Database); + + [Fact] + public async Task Handle_ExistingUser_ReturnsFollowerAndFollowingCounts() + { + var user = TestEntities.User(1, "user"); + var followerA = TestEntities.User(2, "followerA"); + var followerB = TestEntities.User(3, "followerB"); + var followed = TestEntities.User(4, "followed"); + await SeedAsync( + user, followerA, followerB, followed, + new UserFollow { FollowerId = followerA.Id, FollowedId = user.Id }, + new UserFollow { FollowerId = followerB.Id, FollowedId = user.Id }, + new UserFollow { FollowerId = user.Id, FollowedId = followed.Id }); + + var result = await CreateHandler().Handle(new GetUserProfileQuery(user.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Equal(2, result.Value.FollowersCount); + Assert.Equal(1, result.Value.FollowingCount); + } + + [Fact] + public async Task Handle_UserMissing_ReturnsNotFound() + { + var result = await CreateHandler().Handle(new GetUserProfileQuery(404), TestContext.Current.CancellationToken); + + Assert.Equal(ErrorType.NotFound, result.FirstError.Type); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/Users/IsFollowingUserQueryHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Users/IsFollowingUserQueryHandlerTests.cs new file mode 100644 index 0000000..2d7f9ea --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Users/IsFollowingUserQueryHandlerTests.cs @@ -0,0 +1,36 @@ +using Musify.Application.Tests.TestSupport; +using Musify.Application.Users; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Users +{ + public sealed class IsFollowingUserQueryHandlerTests : HandlerTestBase + { + private IsFollowingUserQueryHandler CreateHandler() => new(Database); + + [Fact] + public async Task Handle_Following_ReturnsTrue() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id }); + + var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.True(result); + } + + [Fact] + public async Task Handle_NotFollowing_ReturnsFalse() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed); + + var result = await CreateHandler().Handle(new IsFollowingUserQuery(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.False(result); + } + } +} diff --git a/backend/Tests/Musify.Application.Tests/Users/UnfollowUserCommandHandlerTests.cs b/backend/Tests/Musify.Application.Tests/Users/UnfollowUserCommandHandlerTests.cs new file mode 100644 index 0000000..e853dd8 --- /dev/null +++ b/backend/Tests/Musify.Application.Tests/Users/UnfollowUserCommandHandlerTests.cs @@ -0,0 +1,38 @@ +using Microsoft.EntityFrameworkCore; +using Musify.Application.Tests.TestSupport; +using Musify.Application.Users; +using Musify.Domain.Entities; +using Xunit; + +namespace Musify.Application.Tests.Users +{ + public sealed class UnfollowUserCommandHandlerTests : HandlerTestBase + { + private UnfollowUserCommandHandler CreateHandler() => new(Database, NoOpLogger()); + + [Fact] + public async Task Handle_ExistingFollow_RemovesIt() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed, new UserFollow { FollowerId = follower.Id, FollowedId = followed.Id }); + + var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + Assert.Empty(await Database.UserFollows.ToListAsync(TestContext.Current.CancellationToken)); + } + + [Fact] + public async Task Handle_NotFollowing_IsIdempotent() + { + var follower = TestEntities.User(1, "follower"); + var followed = TestEntities.User(2, "followed"); + await SeedAsync(follower, followed); + + var result = await CreateHandler().Handle(new UnfollowUserCommand(follower.Id, followed.Id), TestContext.Current.CancellationToken); + + Assert.False(result.IsError); + } + } +} diff --git a/web-player/design_handoff_musify_player/Musify.dc.html b/web-player/design_handoff_musify_player/Musify.dc.html new file mode 100644 index 0000000..6acb0f6 --- /dev/null +++ b/web-player/design_handoff_musify_player/Musify.dc.html @@ -0,0 +1,2801 @@ + + + + + + + + + + + + + + +
+
+
+ +
+ + + + +
+
+
+
+ + + +
+
+ + + +
+
+ +
+ + + + + +
+ +
+ +
+ + + + Cola +
+
+ +
+
+ +
+ + +
+
+ +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+ + + +
+
+ ! +
+
+ No se pudo cargar tu música +
+
+ La conexión se interrumpió. Lo que tengas descargado sigue disponible sin + conexión. +
+
+
+ Reintentar +
+
+ Ver descargas +
+
+
+
+ + + +
+ +
+
+
+

+ {{ heroGreeting }} +

+
+ {{ heroBlurb }} +
+
+
+ + + + Reanudar {{ heroResume }} +
+
+ Ver resumen semanal +
+
+
+
+ +
+
+ {{ s.v }} +
+
+ {{ s.k }} +
+
+
+
+
+
+ + +
+
+

+ Continuar escuchando +

+
+
+ +
+
+
+
+
+
+ {{ c.title }} +
+
+ {{ c.sub }} +
+
+
+ + + +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+ Playlist destacada +
+
+ {{ spotlight.name }} +
+
+ {{ spotlight.meta }} +
+
+
+
+ {{ spotlight.blurb }} +
+
+
+ + Reproducir +
+
+ Mezclar +
+
+
+
+ +
+
+ {{ t.n }} +
+
+
+ {{ t.title }} +
+
+ {{ t.artist }} +
+
+
+ {{ t.dur }} +
+
+
+
+
+
+ + +
+
+
+

+ Mixes +

+
+ Generados a partir de lo que más repites +
+
+
+
+ +
+
+
+
+
+ + + +
+
+
+ {{ it.title }} +
+
+ {{ it.sub }} +
+
+
+
+
+ + +
+

+ Artistas que sigues +

+
+ +
+
+
+
+
+ {{ a.title }} +
+
+ {{ a.sub }} +
+
+
+
+
+ + +
+
+
+

+ Populares esta semana +

+
+ Lo más escuchado entre quienes comparten tu gusto +
+
+
+
+ +
+
+ +
+
+
+
+
+
+ + {{ t.n }} + +
+
+
+
+
+
+ {{ t.title }} +
+
+ {{ t.artist }} +
+
+
+ {{ t.plays }} +
+
+ {{ t.dur }} +
+
+
+
+
+ + +
+
+
+

+ Tus playlists +

+
+ Creadas y guardadas por ti +
+
+
+ Ver todas +
+
+
+ +
+
+
+
+
+
+ {{ p.name }} +
+
+ {{ p.meta }} +
+
+ {{ p.artistsLine }} +
+
+
+ + + +
+
+
+
+
+
+
+ + + +
+

+ Descubrir +

+
+ Rutas de escucha fuera de tu historial. Se renuevan cada 12 horas. +
+
+ +
+
+ {{ g.name }} +
+
+ {{ g.meta }} +
+
+
+
+
+
+
+
+

+ Fuera de tu burbuja +

+
+ Álbumes que tu historial nunca te habría servido +
+
+ +
+
+
+
+ + + +
+
+
+ {{ it.title }} +
+
+ {{ it.sub }} +
+
+
+
+
+
+
+ + + +
+
+
+
+
+
+
+ {{ listKind }} +
+

+ {{ listTitle }} +

+
{{ listMeta }}
+
+
+ + Reproducir +
+
+ + + +
+
+
+
+ +
+
#
+
Título
+
Álbum
+
Duración
+
+ +
+
+ +
+
+
+
+
+
+ + {{ t.n }} + +
+
+
+
+
+
+
+ {{ t.title }} +
+
+ {{ t.artist }} +
+
+
+
+ {{ t.album }} +
+
+
+ + + +
+
+ {{ t.dur }} +
+
+
+
+
+
+ + + +
+
+
+

+ Tus playlists +

+
+ 7 playlists · 564 canciones · 38 h 12 min +
+
+
+ Nueva playlist +
+
+
+ +
+
+
+
+
+ + + +
+
+
+ {{ p.name }} +
+
+ {{ p.meta }} +
+
+ {{ p.artistsLine }} +
+
+
+
+
+
+ + + +
+
+ + + + + +
+
+ {{ emptyTitle }} +
+
+ {{ emptyBody }} +
+
+
+ {{ emptyCta }} +
+
+ Volver al inicio +
+
+
+
+ + + +
+
+ Sin resultados para “{{ query }}” +
+
+ Revisa la ortografía o prueba con un término más corto. También puedes buscar + por álbum o por playlist. +
+
+ +
+ {{ s.label }} +
+
+
+
+
+
+
+ + + + + +
+ + +
+
+
+
+
+
+
+
+ {{ curTitle }} +
+
+ {{ curArtist }} +
+
+
+ + + +
+
+ +
+
+
+ + + +
+
+ + + +
+
+ + + + + + + + + + +
+
+ + + +
+
+ + + +
+
+
+ {{ elapsed }} +
+
+
+
+
+ {{ total }} +
+
+ +
+
+ + + +
+
+ + + + +
+
+
+
+
+
+
+ + + + +
+
+
+ + +
+ +
+ + + + {{ n.label }} +
+
+
+
+
+
+ + + diff --git a/web-player/design_handoff_musify_player/README.md b/web-player/design_handoff_musify_player/README.md new file mode 100644 index 0000000..1591004 --- /dev/null +++ b/web-player/design_handoff_musify_player/README.md @@ -0,0 +1,325 @@ +# Handoff: Musify — Web Music Player + +## Overview + +Musify es un reproductor de música web: un player permanente, navegación lateral, y vistas de Home / Descubrir / Playlists / Me gusta / Reciente, más estados de carga, error, vacío y sin resultados. Su rasgo de identidad es que **toda la paleta de acento y los fondos derivan de la canción en reproducción**: al cambiar de pista, el tinte del hero, el glow ambiental, el sidebar, la barra del player, el logo, la barra de progreso y los acentos viran en bloque con una transición de 0.6s. + +## About the Design Files + +Los archivos de este paquete son **referencias de diseño hechas en HTML** — prototipos que muestran aspecto y comportamiento previstos, **no código de producción para copiar tal cual**. La tarea es **recrear estos diseños en el entorno del codebase destino** (React, Vue, SwiftUI, nativo…) siguiendo sus patrones y librerías ya establecidos. Si todavía no hay entorno, elige el framework más apropiado para el proyecto e implementa allí. + +El prototipo está escrito como un componente único con estado local y estilos inline. En producción conviene separarlo en componentes (Sidebar, TopBar, Player, Queue, TrackRow, MediaCard, sections de Home) y mover los tokens a la solución de estilos del proyecto. + +## Fidelity + +**Alta fidelidad (hifi).** Colores, tipografía, espaciado, radios, estados y microinteracciones son definitivos. Se espera una recreación fiel usando las librerías del codebase. Las carátulas son fotografías genéricas de marcador de posición; el contenido musical (artistas, álbumes, canciones, playlists, duraciones, reproducciones) es **mock data** y debe sustituirse por datos reales. + +**Regla de producto:** la UI nunca menciona la procedencia de la música. Sin etiquetas de fuente, proveedor ni "powered by". Para el usuario esto es solo Musify. + +--- + +## Layout global + +Altura fija de viewport, sin scroll de página; solo el área de contenido y la cola scrollean. + +``` +┌──────────┬──────────────────────────────┬──────────┐ +│ Sidebar │ TopBar │ Queue │ +│ 254px ├──────────────────────────────┤ 304px │ +│ │ Main (scroll) │ (258px │ +│ │ │ <1180) │ +└──────────┴──────────────────────────────┴──────────┘ + Player flotante (absolute, left/right 0, bottom 0, padding 0 16px 16px) +``` + +- Raíz: `position:relative; height:100vh; overflow:hidden; background: pageBg; transition: background .6s ease`. +- Dos capas decorativas absolutas y no interactivas sobre el fondo: el **ambient glow** y un scrim inferior `radial-gradient(130% 85% at 50% 118%, rgba(0,0,0,.8), transparent 62%)`. +- Fila principal `display:flex; height:100%`. +- El main tiene `padding: 6px 26px` y `padding-bottom` dinámico: `126px` en desktop, `206px` cuando hay barra de pestañas móvil. + +### Breakpoints + +| Ancho | Comportamiento | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `>= 1180px` | Sidebar visible (254px), cola 304px, player completo | +| `< 1180px` (tablet) | Sidebar oculto → barra de pestañas bajo el player; cola 258px; padding inferior 206px | +| `< 760px` (móvil) | Cola oculta; se ocultan shuffle/repeat/volumen/letra/cola del player (queda carátula + título + transporte + progreso); el bloque de info del player pasa a `width:auto` | + +--- + +## Sistema de color dinámico + +Cada pista lleva un `hue` (0–360) asignado. Todo el color de la app se deriva de ese hue en oklch. Los valores están repartidos por la rueda para que el cambio entre pistas consecutivas sea evidente (paso mínimo ~50°). + +```js +// hue = hue de la pista en reproducción +accent = oklch(0.74 0.145 H) // acción, barra de progreso, corazón activo, EQ +accentTitle = oklch(0.87 0.07 H) // títulos sobre superficie tintada +accentMuted = oklch(0.79 0.035 H) // título de la pista activa en tarjetas +accentSoft = oklch(0.74 0.145 H / .08) // fondo de fila/tarjeta activa +accentLine = oklch(0.74 0.145 H / .2) // borde de superficie acentuada +accentHair = oklch(0.74 0.145 H / .09) // borde de tarjeta activa +accentBtnBg = oklch(0.74 0.145 H / .15) +accentBtnBgHover = oklch(0.74 0.145 H / .24) + +// superficies +pageBg = oklch(0.068 0.008 H) +sidebarBg = linear-gradient(180deg, oklch(0.095 0.012 H), oklch(0.068 0.007 H)) +panelBg = oklch(0.09 0.010 H / .28) // panel de cola +barBg = oklch(0.105 0.012 H / .9) // player y tabs (con backdrop-filter: blur(22px)) +hairline = oklch(0.62 0.030 H / .08) // todos los separadores + +// gradientes: tc(a, l) = oklch(0.34 - l/380 , 0.11 - l/1600 , H / a) +ambient = radial-gradient(52% 38% at 14% 0%, tc(.26,30), transparent 54%), + radial-gradient(46% 34% at 78% 0%, tc(.20,40), transparent 56%) +heroBg = linear-gradient(105deg, tc(.34,42) 0%, tc(.11,70) 28%, tc(.03,88) 50%, rgba(6,6,9,0) 70%) +spotlightBg = linear-gradient(100deg, tc(.28,46) 0%, tc(.10,74) 30%, tc(.03,88) 52%, rgba(6,6,9,0) 72%) +logoGrad = linear-gradient(140deg, oklch(0.78 0.15 H), oklch(0.62 0.13 H+40) 92%) +logoGlow = 0 0 20px oklch(0.74 0.145 H / .3) +``` + +**Importante:** hero y spotlight terminan en `alpha 0` antes del borde derecho (70–72%) y **no llevan borde**, para fundirse con el fondo sin corte visible. + +Los elementos que cambian de color reciben `transition: background .6s ease` (y `box-shadow .6s ease` en el logo): raíz, sidebar, panel de cola, barra del player, tabs. + +### Grises fijos (no dependen del hue) + +| Rol | Hex | +| ------------------------------------------- | ------------------------------------------------------------------------------------ | +| Texto principal (títulos de sección, h1/h2) | `#d3d2d6` – `#d9d8dc` | +| Texto de tarjeta / fila | `#cbcacf` / `#cdccd1` | +| Texto base del documento | `#c9c8cc` | +| Secundario | `#8a8a8e`, `#8f8f93`, `#919195` | +| Terciario | `#7f7f83`, `#7d7d81`, `#848488` | +| Cuaternario / contadores | `#6c6c70`, `#696a6d`, `#646468`, `#616165`, `#5c5c60` | +| Iconos inactivos player | `#747478` | +| Botón primario (fondo) | `#c6c4cb`, hover `#d8d7dc`; texto `#0a0a0d` | +| Superficies neutras | `rgba(255,255,255,.022)` base, `.045` hover; bordes `rgba(255,255,255,.04)` / `.075` | + +Los grises son **neutros a propósito** (sin tinte), para que solo el acento lleve color. + +--- + +## Tipografía + +- Display / títulos: **Sora** 400/500/600/700. +- UI / texto: **Plus Jakarta Sans** 400/500/600/700. +- `-webkit-font-smoothing: antialiased`. + +| Uso | Fuente | Tamaño | Peso | Tracking | +| ------------------------------------------ | ------- | ---------------- | ---- | ------------------------------------ | +| H1 hero | Sora | 36px / 1.1 | 600 | -0.035em | +| H1 vista (Descubrir, Playlists, Tracklist) | Sora | 28–33px | 600 | -0.03 / -0.035em | +| H2 sección | Sora | 17.5px | 600 | -0.025em | +| Título spotlight | Sora | 23px | 600 | -0.03em | +| Wordmark | Sora | 17.5px | 600 | -0.02em | +| Subtítulo de sección | Jakarta | 12px | 400 | — | +| Eyebrow / cabecera de columna | Jakarta | 10.5px uppercase | 600 | 0.12–0.17em | +| Título de tarjeta | Jakarta | 13–13.5px | 600 | -0.01em | +| Fila de canción (título) | Jakarta | 12.5–13px | 500 | — | +| Meta / artista | Jakarta | 11–11.5px | 400 | — | +| Números (duración, índice, reproducciones) | Jakarta | 11–12px | 400 | `font-variant-numeric: tabular-nums` | + +Todo texto de una línea en tarjetas y filas usa `white-space:nowrap; overflow:hidden; text-overflow:ellipsis` con `min-width:0` en el contenedor flex. Los titulares llevan `text-wrap: pretty`. + +--- + +## Escalas + +**Radios:** 8px (miniaturas pequeñas, chips) · 9–11px (carátulas 36–48px, botones) · 12–13px (filas activas, carátulas de tarjeta) · 14–16px (tarjetas, portadas grandes) · 18px (player, tabs, portada de cabecera) · 22–24px (hero, spotlight) · 50% (avatares, botón de play, artistas) · 999px (chips de sugerencia). + +**Sombras:** carátula de tarjeta `0 14px 32px rgba(0,0,0,.45)` · artista `0 12px 28px rgba(0,0,0,.4)` · portada de cabecera `0 24px 56px rgba(0,0,0,.55)` · player `0 20px 56px rgba(0,0,0,.6)` · botón primario hero `0 10px 28px rgba(0,0,0,.45)`. + +**Gaps de grid:** tarjetas 20px · artistas 18px · playlists 14–22px · filas compactas 6–14px. + +**Anchos mínimos de columna (auto-fill):** mixes/álbumes/descubrir 164px · artistas 132px · playlists (tarjeta ancha) 258px · playlists (índice) 196px · continuar escuchando 268px · populares 330px · géneros 228px. + +--- + +## Pantallas + +### 1. Sidebar (`>= 1180px`) + +`width:254px; padding:22px 12px 0 20px; border-right:1px solid hairline; background: sidebarBg`. + +- **Wordmark**: cuadrado 24px con `logoGrad` + `logoGlow`, gap 11px, texto "Musify". Padding inferior 26px. +- **Nav** (columna, gap 2px). Ítem: `padding:9px 12px; border-radius:10px; font-size:13.5px; weight:500`, icono SVG 17px stroke 1.6, etiqueta flexible, contador a la derecha (11px, `#5c5c60`, tabular). + - Activo: color `#d5d4d8`, fondo `rgba(255,255,255,.06)`. + - Inactivo: color `#8b8b8f`, fondo transparente. Hover: fondo `rgba(255,255,255,.04)`, color `#d5d4d8`. Transición `.16s`. + - Ítems: Inicio · Descubrir · Playlists (7) · Me gusta (312) · Reciente. +- **Separador**: `margin:24px 12px 14px; height:1px; background:rgba(255,255,255,.05)`. +- **Cabecera "TUS PLAYLISTS"** (10.5px uppercase, 0.13em, `#616165`) con un "+" a la derecha que navega al índice de playlists. +- **Lista de playlists** (scroll, gap 1px). Fila: miniatura 34px radio 8px (imagen `opacity:.88`), nombre 12.5px, meta 11px `#6c6c70`. Si esa playlist está sonando, un ecualizador de 3 barras de 2px a la derecha. + +### 2. TopBar + +`padding:16px 26px 12px; gap:14px`. + +- Atrás / adelante: círculos de 30px, fondo `rgba(255,255,255,.035)`, borde `rgba(255,255,255,.05)`, icono 15px. Atrás activo (`#8b8b8f`, hover `#dbdadf`), adelante deshabilitado (`#555559`). +- Buscador: `flex:1; max-width:480px; height:38px; radius:11px`, fondo `rgba(255,255,255,.035)`, borde `rgba(255,255,255,.055)`, lupa 15px `#737377`, input 13px transparente. Placeholder: "Canciones, artistas, álbumes o playlists". +- Spacer. +- Botón "Cola": alto 33px, radio 10px, 12.5px/500, icono 15px. Activo: color `#d6d5da`, fondo `rgba(255,255,255,.06)`. Inactivo: `#8b8b8f`, fondo `rgba(255,255,255,.028)`. +- Avatar 33px circular con borde `rgba(255,255,255,.1)`. + +### 3. Home + +Ritmo editorial deliberado — no filas iguales encadenadas: + +**a) Hero** — `border-radius:24px; margin-bottom:42px; background: heroBg`, sin borde, `padding:38px 36px 34px; max-width:620px`. + +- Saludo H1 dinámico por hora y día (ver _Copy dinámico_). +- Blurb 13.5px `#8a8a8e`, `line-height:1.65`, `max-width:440px`. +- Botones: primario "Reproducir" (`#c6c4cb` sobre `#0a0a0d`, 12px 21px, radio 12px, icono play 14px) + secundario "Guardar" (borde `rgba(255,255,255,.11)`), y un meta a la derecha en 12px. +- Tres métricas: valor Sora 26px/600 y etiqueta 11.5px — "Canciones esta semana", "Tiempo de escucha", "Artistas nuevos". + +**b) Continuar escuchando** — grid `auto-fill minmax(268px,1fr)`, gap 10px. Fila-tarjeta: `padding:9px 14px 9px 9px; radius:13px`, carátula 46px radio 9px, título 13px/600, sub 11.5px, y un icono play 15px en `accent` que aparece en hover. La pista en curso usa `accentSoft` de fondo, `accentHair` de borde y `accentMuted` en el título. + +**c) Playlist destacada (spotlight)** — grid de dos columnas `auto-fit minmax(290px,1fr)`, `radius:22px`, `background: spotlightBg`, sin borde ni divisor central. + +- Izquierda: portada 88px radio 14px, eyebrow "PLAYLIST DESTACADA", nombre Sora 23px, meta 12px, blurb 13px/1.7, botón "Reproducir" (`accentBtnBg` + `accentLine` + `accentTitle`, hover `accentBtnBgHover`) y "Mezclar" secundario. +- Derecha: 5 canciones con índice 20px, título 12.5px, artista 11px, duración tabular; fondo `rgba(0,0,0,.26)` que se desvanece a transparente hacia la derecha. + +**d) Mixes** — H2 "Mixes" + sub "Generados a partir de lo que más repites". Grid `minmax(164px,1fr)` gap 20px. Tarjeta cuadrada radio 13px con scrim `linear-gradient(180deg,transparent 45%,rgba(0,0,0,.42))` y botón de play circular 38px abajo a la derecha (`rgba(10,10,13,.76)`, blur 8px, borde `rgba(255,255,255,.12)`) que aparece en hover. + +**e) Artistas que sigues** — grid `minmax(132px,1fr)` gap 18px, retratos circulares, texto centrado, nombre 12.5px/600, oyentes 11px. + +**f) Populares esta semana** — grid de dos columnas `auto-fit minmax(330px,1fr)`, `gap:6px 34px`. Fila: índice/EQ 18px, carátula 38px, título + artista, reproducciones 11px y duración 11.5px. + +**g) Tus playlists** — tarjetas anchas `minmax(258px,1fr)` gap 14px: portada 62px radio 11px, nombre 13.5px/600, nº de canciones 11.5px, artistas principales 11px `#696a6d`, play en hover. Enlace "VER TODAS" en la cabecera. + +### 4. Descubrir + +H1 28px + sub. Mosaico de géneros `minmax(228px,1fr)` gap 14px: tarjetas de 112px, radio 16px, fondo `linear-gradient(140deg, oklch(0.24 0.07 Hgénero), rgba(9,9,12,.95))`, borde `rgba(255,255,255,.05)`, nombre Sora 15.5px, meta 11.5px, y una miniatura de 80px rotada 20° asomando en la esquina inferior derecha (`opacity:.82`). Debajo, "Fuera de tu burbuja" con la misma tarjeta cuadrada de Mixes. + +### 5. Tracklist (Me gusta / Reciente / Playlist / Resultados) + +Cabecera: portada 164px radio 18px + eyebrow (`Colección` / `Historial` / `Playlist` / `Búsqueda`), H1 33px, meta 12.5px, botón "Reproducir" y un botón cuadrado 40px de "aleatorio". + +Tabla: `grid-template-columns: 32px 1fr 1fr 92px; gap:16px`. Cabecera 10.5px uppercase `#616165` con borde inferior `rgba(255,255,255,.05)`. Fila `padding:10px 14px; radius:12px`: + +- Col 1: número o, si es la pista activa, EQ de 3 barras de 2.5px en `accent`. +- Col 2: carátula 40px radio 9px + título 13px/500 + artista 11.5px. +- Col 3: álbum (o `—` si falta). +- Col 4: corazón (14px; visible siempre si está marcado, si no solo en hover) + duración tabular. +- Activa: fondo `accentSoft`, título `accentTitle`. Hover: `rgba(255,255,255,.032)`. + +### 6. Índice de playlists + +H1 "Tus playlists" + meta agregada, botón "Nueva playlist" secundario, grid `minmax(196px,1fr)` gap 22px de tarjetas cuadradas con nombre, nº de canciones y artistas principales. + +### 7. Cola (aside derecho) + +`border-left:1px solid hairline; background: panelBg; padding:20px 16px` + padding inferior dinámico. + +- Cabecera "En cola" (Sora 14.5px) + cerrar "×". +- "REPRODUCIENDO": tarjeta con fondo `accentSoft`, borde `accentLine`, carátula 40px, título en `accentTitle`. +- Cabecera "A CONTINUACIÓN · {playlist}" con `gap:12px`; la etiqueta trunca con elipsis (`flex:1 1 auto; min-width:0; nowrap`) y "Vaciar" es `flex:0 0 auto`. +- Lista scrollable: miniatura 36px, título 12px, artista 11px, duración. + +### 8. Player (siempre visible) + +Barra flotante: `radius:18px; padding:11px 16px; background: barBg; backdrop-filter: blur(22px); border:1px solid hairline; box-shadow:0 20px 56px rgba(0,0,0,.6)`; `pointer-events:none` en el contenedor y `auto` en la barra. + +- **Izquierda (256px):** carátula 48px radio 11px, título 13px/600, artista · álbum 11.5px, corazón 16px (activo en `accent`). +- **Centro (flex:1):** shuffle · anterior · play/pausa · siguiente · repetir, gap 14px. Play: círculo 40px `#c6c4cb` con icono `#0a0a0d`, hover `#d6d5da` (**sin scale**). Shuffle/repeat activos en `accent`, inactivos `#747478`. + Progreso: tiempo transcurrido (10.5px tabular, ancho mín. 32px) · pista `rgba(255,255,255,.09)` de 3.5px con relleno en `accent` · duración. Clic en la pista = seek proporcional. +- **Derecha (256px):** letra, volumen (icono + pista de 84px con relleno `#8e8c98`), y botón de cola. Oculto por completo en móvil. + +### 9. Barra de pestañas (`< 1180px`) + +Bajo el player, `margin-top:10px; padding:7px; radius:18px`, mismo `barBg` y blur. Icono 17px + etiqueta 10px, distribuidos con `space-around`. Muestra los 5 ítems de navegación. + +--- + +## Estados + +- **Loading (skeleton):** bloque de hero de 260px + 2 secciones de 6 tarjetas. Gradiente `linear-gradient(90deg, rgba(255,255,255,.025), .055, .025)` con `background-size:420px 100%` y `animation: shim 1.25s linear infinite` (`-420px 0` → `420px 0`). Se muestra 900ms al arrancar. +- **Error:** cuadro 52px con "!" (`rgba(226,130,130,.08)` / borde `.18` / texto `#e09292`), título 20px, cuerpo 13px, botones "Reintentar" (primario) y "Ver descargas". Copy: "No se pudo cargar tu música" / "La conexión se interrumpió. Lo que tengas descargado sigue disponible sin conexión." Reintentar vuelve al skeleton 800ms. +- **Vacío:** icono 70px radio 19px con degradado tenue, título 20px, cuerpo 13px, CTA primario + "Volver al inicio". Dos variantes de copy: playlists sin crear y "me gusta" sin canciones. +- **Sin resultados:** título `Sin resultados para "{query}"`, cuerpo de ayuda y 4 chips de sugerencia (radio 999px, borde `rgba(255,255,255,.08)`, 12px). +- **Datos incompletos:** si falta el álbum, la columna muestra `—` y el player omite el `· álbum`; si falta el subtítulo de una tarjeta, "Sin información". Toda carátula tiene un degradado de respaldo detrás de la imagen. + +--- + +## Interacciones + +| Elemento | Comportamiento | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Nav / tabs | Cambia de vista y limpia la búsqueda | +| Playlist (sidebar, tarjeta, índice, spotlight) | Abre la vista de playlist con su contexto | +| Tarjeta / fila de canción | Fija la pista, `pos = 0`, `playing = true` | +| Play/pausa | Alterna reproducción (el reloj avanza 1s por segundo) | +| Siguiente / anterior | Cíclicos sobre el array de pistas, resetean la posición | +| Pista de progreso | Clic → `pos = (x / ancho) * duración` | +| Pista de volumen | Clic → `vol` entre 0 y 1 | +| Corazón (fila o player) | Alterna en el mapa `liked` | +| Shuffle / repeat | Alternan, se tiñen de `accent` | +| Botón "Cola" | Abre/cierra el aside | +| Buscar | Filtra por título, artista y álbum; vacío → vista normal; sin coincidencias → estado "sin resultados" | + +**Animación — reglas:** + +- **Nada de elevación en hover.** Se eliminó a propósito: el feedback es solo un cambio de fondo y la aparición del botón de play. +- Hover de fondo/color: `.15–.18s ease`. Play del player: `.14s` solo de color, sin `scale`. +- Cambio de tinte por canción: `.6s ease`. +- Entrada de vista: `rise` (`opacity 0 → 1`, `translateY(12px) → 0`) en `.3s ease`. Cola: `.22s`. +- EQ de pista activa: `eq` `.9s ease-in-out infinite` alternando `scaleY(.25)` y `scaleY(1)`, con desfases de 0.25/0.5s (0.3/0.6s en el sidebar). + +--- + +## Estado necesario + +``` +view 'home' | 'explore' | 'playlists' | 'playlist' | 'liked' | 'recent' +listCtx nombre de la playlist abierta +idx índice de la pista en reproducción +playing boolean +pos segundos transcurridos (intervalo de 1s, vuelve a 0 al llegar a la duración) +vol 0..1 +queueOpen boolean (forzado a false por debajo de 760px) +query texto de búsqueda +liked { [trackId]: true } +shuffle / repeat boolean +hover clave del elemento con hover (o null) +w ancho de ventana (listener de resize) +demoState 'normal' | 'loading' | 'error' | 'empty' (solo para demostrar estados) +``` + +### Datos esperados + +`Track { id, title, artist, album?, duration, cover?, hue, plays? }` · +`Album { title, artist, year, cover? }` · +`Artist { name, listeners, photo? }` · +`Playlist { name, trackCount, topArtists, cover?, isPlaying? }` + +Todos los campos opcionales deben tener degradado o texto de respaldo. El `hue` debería calcularse en backend desde la carátula (color dominante, con saturación mínima garantizada) y venir en el payload; en el prototipo está precalculado por pista. + +--- + +## Copy dinámico + +Saludo del hero según hora y día de la semana: + +- `< 5h` → "¿Sesión de madrugada, {nombre}?" +- `< 13h` → "Buenos días, {nombre}" +- `< 20h` → "Buenas tardes, {nombre}" (miércoles → "Feliz miércoles"; viernes → "Feliz viernes") +- `>= 20h` → "Buenas noches, {nombre}" +- Sábado/domingo → "Que tengas buen finde, {nombre}" (o "¿Madrugada de finde, {nombre}?" antes de las 5h) + +--- + +## Assets + +- **Carátulas y retratos:** marcadores de posición fotográficos servidos por `picsum.photos` con una semilla por elemento, pintados como `background-image` (no ``) para evitar peticiones fallidas mientras se resuelven los datos. Sustituir por las imágenes reales del catálogo. +- **Iconos:** SVG inline, `stroke-width` 1.6–1.8, `stroke-linecap:round`. Reemplazables por el icon set del codebase. +- **Fuentes:** Sora y Plus Jakarta Sans (Google Fonts). +- Sin logotipos de terceros. El wordmark de Musify es tipográfico + el cuadrado con degradado. + +## Files + +- `Musify.dc.html` — prototipo completo (plantilla + lógica + datos de ejemplo). Contiene todas las vistas y estados; el selector `demoState` permite ver loading / error / empty. +- `support.js` — runtime del prototipo. **No portar**: solo hace funcionar el HTML de referencia. +- `screenshots/` — capturas de referencia (viewport ~924px, es decir en modo tablet: sidebar plegado a barra de pestañas inferior): + - `01-home-hero.png` … `04-home-populares-playlists.png` — recorrido vertical de Home + - `05-descubrir.png`, `06-playlists.png`, `07-tracklist-me-gusta.png`, `08-sin-resultados.png` + Todas con "Yankee" sonando, de ahí el tinte ámbar: con otra pista el mismo layout se ve en otro color. diff --git a/web-player/design_handoff_musify_player/screenshots/01-home-hero.png b/web-player/design_handoff_musify_player/screenshots/01-home-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..deed3a9d70baf915b960c47e06bc24e558e6cf9c GIT binary patch literal 32701 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
j4IG8VMH+?VcaQb@$3 zFr&z`_zI;y$6xnk9a!!Ke}g$8`#?;(28Nzcm%2MJF+_+Z6iZaDEF|qi;KIW#h=;u! z!U0ocsa_(2c$M4;b_-kz2F{rJbO*D%3ocGl6)zl`{{egIq@KHreXffQU`1?V1XbuH zp7``>gTpIh3V!GTJ}sqq8}#e>#8kA03guF$xy~GlBi&ie*t@Gm;0F?42Z~Lk*S8^z zM=h>rAT!WX5IQ~ERSk}>N-0~y1PHykr{y&@v8oj|C$S`*U5?x+`bHdwj~~3M)ZCGzpX|3m+aOJ?)#rVWjvV-D z-8BlW=6|80Q@Ufpd3^KkvR$R8rpEJxgA@sSq8^HvvPN9bC#cXx54kK}-~6)K6vaiV z=t4+pc=Ixj#}~>{%nUsf;RG9dh05ytn7ixSp!pS*k&fmSN8SbFX&Syn7m{z4#t@g( zDAg#sh>M>zH13>M%f)f$VPiWJYBsMsbA`&l37AFb4>bt$Pt;kBCgCGd=Is%BHHp!c z@3Get`$}c|!!SR1k_3-qxuE9ZU9J8DZyCX|#k`C*sbk62>P8V}4URq@tt?V`kr~Dq z(TmM#gA9^@M>+qe(L2E>QH$1KYkpda!YP%+MRaHZ0f9F@TUnY$uUz(!3_jO`nsmfI z&z01ak9m3m|5?HG7!mFaRlwflt6V2lj{D5^-+3j-zlca5(M_|!xEDfXe81MV-b$6I zVaF2To(9PbE#*LECvg=flzfzQ3DhuD@#zFEt$a1#ULxbXl!@0_S;e|~c?9*Y=0NhG z4(EObDJ|}vd)wqq-)2?79`Z>ZB4or+6iRrf3>XMbE)xTfbEt$t==8L5<)oGLcdmWGo2L( z`1_58Kxn3a*SGx{?;JO{7BzEFNB6{vVfb*Iv*>yp=)~#nUfHP49tLdmMp4`%@~ppm zykM8Zyfg@`NeoyW+)-qTI#Z2ev18-nb;USs5R>q?kZ0PQzehjYYs+rxk}5%gKt`Zo zr=Q47Rh|)s56A^|BE9;78?*CDQqLkDIk7=>>L5CFzJa)>&IU|Hqfjpe-sG`F*dfg~ zz5VcxQJzUVD-{RVG3R}v4Nj{u3teq1*WUzj+4z;6o?gY@a3Tpsr(|orY@HWnWTwtm zhi!>eEMYWz=2c!{=yRLCo8CcBWHqgidTl_J6E78xyW%n)INy}WdN0LHmG^U87;kdO zN|Gr()6pEG`0!d0CU5L9?A+CE&<6k^C{udYjKvAa1L!CqPTn`VV5NVA>@Oy#aHkK0 zSOP>3w#D3uK$z2_*2U zz(i+9EA}@pmf%68t|QIr60XY<+!k7zP0 zz?MD;xq)Ljcp_eb%~rnQxOd6I`OYDw9*|tq_*&%4>*U|mCrxAWrN60F4!^uC{p`O9 z`AYe>-7_;{#{SXo8|zvW|X0-p)2W7;C-_`L}6r^mT2e_{p!CX4(RWDe8?{|c-U z@9)NRWXTpVk)#zDf0(O7haVwAqajMg;|?P_H?^G4yG)m1UlfHHkEnTK95WpQe^Iyn7n%o))JvF`SR5{8tGD8Hq257ny;6}sJzh9+~E6s zvh-3&+Jdq^l~G!dzj`A%pLYp9h$Z1mHC}A74bu>587)*d`2aJ%;JxzykXFLdqVkdx z3-x5P_(!#JZPIKfFqudP!xxR?^h2Z0{CDep{y_a5WI{zUPD08~lK356z-u1mbFItr zUQ)(+6sh&$UgH&!*xA|F7IEh7WRl(}Y)P!px5v5Vg^sNpjP2YQYtC*l%xuEnivxf| z+<7miq5NGLaU~*8)|02jWJx)US@_z+^OG6CYZ33!o)m>egW>mBzPd zM2&k6xSo7jcfJ8~{Z!tg^OOp^$ERz*%L0hy``WT)N15`B`?pW#{!ExaJT?=K`)WiB zvkSl{vlC~2DF}QzsTJ3Ob`P@Tqse?>ws~7dVt)z9u=}HuEY+@RzJA_@^6>vR{MT%QgKRvUf(=$1112-OWC-`^H1YZRd)*M(# zOc%GJbN}GI2(f&ZN)z3T#E6($z#~)|WItoyX)TsfZw?{8e(9)bQJl0<>RaJ+k}{}^ zI}49Bp8JB2b<-@ZS6AiwY`Y|h@Qg5n6o!_cs=m!c$VAfkF`jQqH24t(6eOO=y&}@_ zG5_I(!Xr3EKk*I+l;4VmjH_9{z6G(p%=++}GLKNfP+gVvFe;R24m-jX-bE}bQDEyx zQWW&?=*-LSjX1a78d9d&hC_6H2+3XJnKqtuBu3gwY52-8z4*OTyAW&dd6TPf&xDYh zAuCQ#@`!O}iad^yDvXB*giCf+U@)&-Xensn@`eqHk$Y@v+;p_hX9i z`7$2k821=A^@Q;^eF$or7~!a9#hlsryi#X*B-z9tI|XZrULH;nI`tR9H7pI&3sc9I zFiv%=gF0rU$u;oAg+7S)xB!XGC^m9*w8u_HnoQBjfU7x4g5~Rh*znHT2t`z>E<4-u zUr%2{hb-bGFa_P&>hQRRzO`6HQ-M?y-Y%<7tpBc?xm@VG$qs|`ev4SY*pImn17thQ z;xvfF7m9ddbmvzlizN2KJMFKVPq_n%1Pv7py2^&Tc~K@DlJNy1Ic@IZHZ%Nl(%8vF zAFogi%p(K4N7`2@$w0-QxrVrx8{lVz_0YJR}1(k$lR^fgw||G92Dx zK)L)p6nv7_->oeBn>*n_k>KCiu4F)@nHW&a^V!@wfz`qXl*0q$78-a;#)}O{)R`h}{Mu8mz49n%c3%wwhxMH62{*+`{(tW*! zBrci+*5_;0?Ar!iw(V8#!>llj-Mbkf<)%LF0z5?T=WEJO3SyV%xC*jQD7F`;jtn;r z6!o>&%RLpkBE*8KhG-Z$kC#+reb~4FGTdwH(FcG^N*g51v55>AReSOE4F}Bc^N)#Y z1z#K9G#cQP3!h~P4|EMFjyj#ZWkZXrFnuL83ul1iwRU8KCMe@P+y7Ure__e-R|U zRJKC8RfPjg;4oxmxGt2w@V{aj?-Pc6<~sFdc3#lNdQjMzbvFCl=VLoHu>Lorclrsu zpzL#KpwB})@J~Rq=-Z&+o%a>*^K^?1xn5l14{AFkXzq7CHT}8qNBWz?+Q4$@CtwlF ze067$DGb_K3kNgB0jo?9tLd4zx1fIjE%N9~t*W75{eEjz z>nxot%}eD^ubU3p{XgRW`SG7F4pzHD7uXf5VxtH<Z23D~g4p6hV%H^eUkV3U~k$4naZ>O$Y%J2!Vv&RnQ|+6apv&6herh zN(miN>H(A*id5-EKtaR`d>i$6&U^2D@4dh0`F+p#{c#4fXU)u-HEXZ6rmdOn_Tcs_ z;0R0)st2&J000a42W-DSb_;fy^(e5D29AYceQ=H-ECj|*C`C^P;3#k;8~}0v)l@IG zZy6w#)ekKF?^w8h#quF|sPAB^D?2GWD>*Blm2px$i;_`thA7E6$U~fD6wWFrDJVKB zp_SCmf^D&pevp&+PR@b&pBj~QAQI68Zw%gp2vz_pe^mJOTLniv;qMi=cG{N3c(@SI zP8hU@x2*3@BLFW-k?8vQ_jey3Cu?lqF@0Svb){|u%*Ua?`V@>4O5GVxz&UthF*pYo zv>etG?SjR45WO8dP-y6{{?h(9Pjw8&N!{VBGD;Dp;v}P@tl}u62~YlDT>IZM?tuDMQPx-SC)Wo?U)P+| z<4U>qhj((LfeolDmis$gXS)1AzLx3y9di5j?s!kM2ige>w(EdHd!q@&47b0;LA;uY z@*%kU%Mj7-&Zl?yan}4+>n{<@{bva}xjGPCErqS`MckhJoStX7M1J;DwiPQo~c0AW`LU$iiafOhakI|<_n!Z^GW#u)>`JJr6C z02}uPRYsJ>5%JF6exUxeUBGmPlLsi!A59Y++}*Xm>k8_DatB|}={=~iKYnOf>)&g5 zN0hQo4&Dww6`XQ66BV{9*QAegf6V<_EDJ0M-%&6XG$j{jqN{@cAj-~`{P@mKhv zl->Ih12g8$bv)gllM+In=FD`ZzSNM5A0PZ(#?4m$Qspa2{CgR_#1nzFLI z3@A8=Bg)|{1fq&2{7||5Uxt4D|HZEGowVfN-u=JqWq)<|AKK*NR#iKG)Jptf(C5(x z^J|PsZa_z6F3W$Dy-LyJ;okG#=}P=hbblJz53*)o1hcAScGre`b5G{qcRaD#aPz8k zC`(HSM~oyC=f4=ye<~ET=Og=e%%>;9ij^~ycW9;$F$ zo@Wo6*)DdjLqGYI7QU==5#&;Or+|%1^k;$a#qT{__#!w-fIS=I;7$}q;PKwV7q#Vu zj&mp~%d5((D5xr(g(xGzq$-$HR#5wvl>hbLHzgr2_Pr_LzcuxJr24Z(6R=-^qp`e| zJlCGR-`Hnm`SBiXEPL5>fL-?j+1=VN^X}&EQ>xXWs#*WKFgQ9;{0X*nysJF>m=w!d zKPSJR3sJ|E%+hB=>R`ufRmu}6mh$HEhd@3A53{gr@7l%2x;Olr zYy?Gj9g;t|hh1t9%PyAd^LDH(UnhyI`t--C z#?F-Ux&`gg?b>9Pb4jW=+tQoOrq1SRR zXy^{Tn->{)|4A$Dx>+-;yr;d*=zE78M_9HTDKeMia;y030Y&?)1kEqV(Zjpqq#NSY z6$@_p&zpQUHJ+%|-=DEF@>y}D|26LY1N|d`envCDk`ygY5z!WNgcdoniajpH5k$^G1aTrv-So}+6x^dY!utot%9 zyrGuO!P2^aYW(RPgBv|kE*58>m8iaWE4)3)G6=nR(f%^RNDum(?sp&cA3LwK3-l@A zJiG}AeH~#-E2Qw=SKpo2J7o?iqUTQhZvyDl-ss2>0U+>9WR^V%yspgen;c{g05b>3cnJQ937HG480n>LxhFr_;DeA zp>x0daxU0J&m{Q&IBY)!j_zYU!A53dIR&sDWnnwYvfT&>fZ}Fl+c^{Y8CZ6)vhCip z7c9JV1Yl)hW7)NP-@aYD*}!a2Ijn5Ej&kn)MZuVhTifC8o>K@DulVk_Jc?&cuO?&^ z9XqY_ObJ5FEOsPJT&wR{{S_sm>;zIe&axAJHn9VMl)!?MM|bi;%4|PNS&p)Dg7n$A zv>mpGfP-KG>ru9&zy)CSDXY?V5_hyk0AR=>EExiJonhMsn??)#ItsLB^7`5!Yr|G@%u9@g3?$_15 zXN+s5ZXO%s{0dWOpKZ2RMkB}zrP)R4ht-T$ZKHGXZ0ZMI}z)v=2h>J`Ykqd#}cNkFwj*2FduDH)FLBe<5SD5wT zPV_H_l}Gx8P9Dkg)~{=HjSIVV*ZG|K znyI?$u&MVdd0}aGx8cz@tNf?OC!Jgnqy=%crY5*LC7r3A`)knGOC%MvKVP`gHK(F8 z)qk*oGBStBTe&ZE4H9x{7T+3?>xY6^XvJ`K6e_0S2{8Z5B|x z@W|Lg&dg{`#isKrE?S=-S{ZO*mLy^Zt`Q1*lzM~j>OV;2FUrn=k^0m;6K*$rHFU}I zB_uu1aHjN|%FgWOGlDyo3=bL)4WI82P_RYTr>W@`+>$gyBji@yem`;K+o-%VhV6vi z-y^`CqCN2ZO8=8T>J}Apcf&m-VV08v2d~}&SndcPfu?+Z^z`17W*)L<7URrR+QKn8 zJ39DAwrA>+lGh6?XUzfFq{HDnv{+oTACrI^7V<$xEEF9jn@8C!ACXUT)iG} zMY|}()a|Uj8OxcEx_#x{2i*|uagewNTw1J6Fe1&BRC|8^9DHRQzabT$)q_z^m>Sa( z$p5f@*P1Fmd4bhIC&$YK$(@xXR(;{>Lzb@jeoUM9hu3bL*_6asy_`U7GbhO*R}m@_ zj~9pvKL9&R*zyr?norVTJ~*Tos6y>-$z2%N)Epspik<>0D=T`tIb@SUb2_}p(;b(u zWv}j8m~X?TGgcnh(#)bj}OEC-`=Q?=R#0+qh9You3GVx*#^1e=4<_Ilgj4{RR0_o@4H zfodSP1<$ipCqpyuscwYHyO{?NgYtE8Kze{C6zdUhs7GnHz3qE}^)aCKg)i^r;O33T z;7R}-@cvZt4;}ABxH#r5k&D5{y@Y-8a?B#*Nn)NEC(66Wn^Qg@Tai<`ZWhG*RmFKz zUt1TGlnUfZvw)7&DEe{8z3k%Z*rw6^WgPunDh2}m)Kx(JT~BrH<&r8zj<>xC(O>S`($2irja5(Rror?05wu60S(AUD}CX( z?buJBYigoacifF_e?nf!@t{1n=)*sL-JP!{GGVS7MD|y5tk~)>Nlpwjhs^h9Q}-H4 z35uvptxDOsx0RY<-{nk~mQD9>ZbmPdSJ8FPE5^dl4-6IOY7f(%7Of$ePw@fbAJ54lr{c-mNMJa`uhMGd;A|ucg zW0wBN7cs5pi40SgI_e=&Q80I%3{>i4 z$GReZfma(xjPy@`ookva|GjD`+0Q#ovX3i!Cik~1@tvA~WdBWv060s14`*peZ;I$% zYWwG8PD!SZO92!c>{;>Q9$0z{I10{FSp$3EwC9a1!r{9T=ElvZ4XL8RA8$l%4z+2Mb*LS(VW{X-K#- zI`9(suOE2%y$?m_#XB54HqCe}-Z)HE; z({VRYuF?B;cj%da3-gp@+1*|}WrA~%J`K*ZyPDxLsx^=y8*1t6-exZiq6DkxlpJHR z8o8KdV_GWqlJhpM=7}&XR1*LmhJ>Gg3J6Ga`<$97P=HeBoG6ycxdG0BGrX?;0i7F1 zGUzHj+9K;Sb3t#}E-N>W8JW}d_|deES)B~&7tm(*!yK(Et$QBh7S|TI&z@968=Oly z6j5>zrfA}iF!I%69vqbrZ(VtZrrw8-KGW#nr!=dLg)=#dGQ>m$jjdv9p6Dovs=<_I z_!xrL#m0{WP&x?Dv82+WPUn4zeEJ54hR_Mschg`4i8YzjTRQLyNQI#8M?b{{_Tz6| zd1pNVup&fc74~THHu3?eBTg*Q+IRN+NGj|K7v==QFR`+S{JrPzKC)jJoSc7B`>z33 zZ9r0cS2zGjU#9JURw-SR*ggzF5aZxOwA>EYUHK?(GQIS{o_95Lq$Gqts$M_)NmX$L z!!yQ6T}r;u{v_KAn~;)y{T>;%i0eqx)Oddk32)EaR?Q#c)m=@Z zPeY-PI;{z#Nv;4s4T)8pf3G4z*EHZgmG|gHo10xFzPEb+7enC#M+*1#MW<&aY6`>$ zy;b+&>o6vzdesLa8almfSTLhHW)je*mfw&`$K5*F4y7nDPeYX{GPrhMtA{LgSyT=J zEx!$#m11eN`Qn?L!bLz^`)Ru?k1^ic;T3o+>KF}1s({A{# z$v2LS(*0=P*13z?<)a0#@)*vmiIFv9e6Q7F?m4=~pP*hHT;wtAjw4ByuU z0NNtpp7B4*{O73n-Pr$^a{no70=(?$Ho#{zEcfy1g6uZ%Y+xIJ+ZRn1oPU|#|K_V; z*!s*DA<3!bd;Bqvzd)A52AQ~G^Gnu!+p7c`mM5n7i~x}Jm(_TETiNjX0%%{ zs|aoKcC#Cw8RU7pR2gTT8t8)?dNgA~A#slH9US+%G{1EFkCz>Dwibr)2+9eU;A+)S zG4|yP{ZShb3^V6Zv(G}0f1;)KLZ`<&Yet1>m4!|YE&6rWY8+ERthufZ9D6V0))C{nK;L^Kwl3b!gu+1Ss$`ZT z-D;(?eSIVM>9?glFgXA@Eh_s}Y<1tN*7_JyPaz*e6`gIr_C|Lb5OLshzpuhQe(7Vu zwC+HZsE#38x3t_~tW9!Ho*lo6?WsV|^%yTdhH zHT_7|>O*f_WM~*}YNUixQGwipfVnc=uf9Huh3dAFJ^kdV-vte6k8$oBiZwJu$s?wn zNWaR~-n8>EVWkc(#eLI{Uf(tjJp26JsLTb44Qee!yNe2wVK#J_Pf}hqD<(8Lju(M@ zF}@Y$*kQ&XA%@DxNsVfcwo5$O{pJky+y#j;n_QjrsMR*ZP1p9MpvywMiO1z~Qa6Me zS|!nYlQxR6=yR@Jf1hcfgP5qy zX8N<4NY&n#W!+KMIVcG~d5v&Qv$(`#Wl2in=di_zs^YDAB18JH=sb?ZvXP^vm(-JiEm7Yu2mF zYGVdVIfsUJz7}Y1sMr6stTkCfSe2y04zlc6D( zpcw7DnN5orNuHeSIUl`nO!RV^kZZ1b&`fO0b-MTlcl`5QS(B|ls$zX$R!eHBj!9_` z%)VUk_M6$2{w}UOCH6(mgZHY`$ryY@r`#DAcp|KFenJ8jdz_8+g=#Z{*JOa#5!Wsw zt2$mYJ6|YX(b^2nuE^F7%;SP5kOVF3rA#nlbwy8RqX)F8qCAHAM&=*iu5@m&kq_J& zUWk~c%#~74>dMNkj($!R?Z(EMXw46|J5vb96ILS0k8TqJq=sKpJ9UcdxNQ=_o zgo+JMDU0qNeRU$ObAB~?Yb3m$8*6GsgClHIr0=quiHY&n8x}~&?itk2V)XK9Lo_B$ z3`%N-k;eIZ*@>}P{nq{i)qL%&xipA`ZauUKo#&~elw-v2AeQ!eUpaeD6cjG&nbjN6 z7WC2Yz6#8ggn>($sW~dfTjKJEpVGR_1}K!IJQv>aYoc(k_~;B#v1h0NLP9hq2SE}Q zYl6?;Xp9+l%xD9Pa#^by?Ect1A`5JCC@wpquQ8eWt;{c<>eyly&kF47e zWT&j2-ec)PLDkW{G3tpysrNmk#cuzdsF5!|{?Km%i94KY|hxefMUL^mfnIpi5 za;1s#4gUEoQZ|V)Dmu6gpc*YoD#~)Z2PmpWsts``T@*Xohd!K>#r0rKXWrA>uYWu{ zv=l=Vq`OIWXD?K=8^q?lfh<3Oi^UHMF(fyAXH&D|n^apJYBpZAO>P}OZ-H#)gmV)Q zzQbvxY?O`HktGos7{t&4Ni&huu(GM=y>v;l(}=@dx7&j6Sv*?nPt;q+K2m{8LfeZp zTQ0UDA>vDsJ=!fcBk>`RsQ8&k*+{8W%=#_7r`zF{t9Iutm=-)@QfA^%(S7UpdOzd$ zEdbl7quKV~pL%BbL#m($Y%5 z6y>tf#LX9spg=-~I$uLy+KfIPhpYOcxZ3M$I=5fC8Mk^Y6@|6EypUgJt76czz)hUA zC||1reSBj>YEUoF2BWvZ?9~=+w?;1~9)Q84MIKL&lLtiqI7Ev->m$9A-2=Dam4D;GPVG=cc(PbZ@K)RDdPXt#tUHT2aLjqIef8`L(> zFnZEhd3w)a0OQ?wDncR*p|QbSi(3#ll-n1mxv;o!wxN1Y5SRP%0fW%kV+`5l$+wRm zqV?r28w!k*bj^+ORkE(9duEO^crUsw%TUa@ad~36 zy`lwq@2WH_M#@uaN-E|ThIBZabX6d+Qi2)Bd>JFb=|x$2IC?&!H>uondG^JY(Xvz; z-3`Hbp?Z<4S8#vU2fs3}g#as9ew}`pQ%()OnLqi}bB7rs&1}9n>LKNKlj!xSx^Vuo zVBdU!FV$&jx@K9gVP|j0H#BTa*WqW->hGs?W}A)sMEBmpjyGJtG`X}~Ywa>RKb9AqD)pZ>DST9{a^IM!q8Av3`ra@vb@y3!>@H%ug~Bl$By zVmTz=8arT;L;8iI>)O)C62ZHrq0zM#g2xShuUP1##f9=(Shq_db3Pwe2fcOTi%W(o z6%S71u+?qJNFP``ZV+|SagWTpvo2_!F^zu!s zsf6B^c1giwXg8CrmffOpC%>|EF{tY2fAjWnoPyw`X-yu^LU8j1jR)5LV!q1e zSqnU0!)xRdi(lKDceE`soG-|2`6N}&wnewwSyJ5xJ24(%{69~_fR8UX#BcwRBd_U9&YbdH7A8*4I@J#2&-sVD6O7w*23EH!} zC?_#VV)UyK^l{PvN;C3iQkSVIetC9L^rpJyyShF7hfh-La_3|xGVN}^JrIY#AYJBO zak=Wl>22VT+{2RY6!rX`ig(n+*_PzeHKgc#9m@_P|GoD<8jp%G3lw|J)wsr2%bGM3 zS3_rYiI&Bd3%L@m$(djG+zOO?ik&)w{)P`$WId>zw3D|}2sHBj#QDFU9rXZleqb|t z#Or+x6H8{ChkA-P{)g=+xcO8Uka8Pwr?>VPlf~X=$R1o-s=ZsX?E4#jBZ5a?{B>Y4 z)8tjrkok?S!9us5Tmc_=1g7+z0U^mxLpqUnJOJ@VI%l{VawuNi$NPTE#PyCeBXL>j z0{r_yU2zPOjH0S6sa;`_l@S@joEMx1c0DR5pIL3$8`Nrjz~Eo(d%u zCYjBgFlNy96G`MJds$aj7|Q|X6{+NLylAV?wH|Ovy?AAv$KlX}i-#fkEaXLI3j9UJ;efy0#Z;$L24m>QmB;)Slp6{B2#UdY%eZBJT)nD%`4Au(= zfB&1n;xVu^Ui!$I@kM2s%?l2iv?p!HFQWB7R{q;kVHAx_tDTW#VAk(Ml!#B=%DNVp zX;z!T5mhkbLDlY!u9B8LEKqr3IkntIg6DEP)ko$ahaHzPrs5`{R5ZVJ*nvCxNtLUF@d+^RSZFw6Q zxh-V14J2RQ2CTox%PwuwFy*d&v~CUk*Cvk=M!p!Sxjde-Ze#C{@6^^^Ob%SKr=$KT zT(z*@kQ-TW|9m$t@f<8Q)O+e!-L5CXE~RCo*-c&y3O)zw@~Z+5H-2y+Dd-}#s*Kug z?jK`06-Iosk#66XiY)bIz)Eh~*76bTj=44q}vE%$c@|5%u7k{3l=dnF& z*((rYS8o!XI^3{te)+JtOqs3+4Tq(57pmz#($gH-)0)fs$$%kI#xUXtJrPDU?;q~l z@L8YlXeLvdx$v)rRqU*hpDq^LjxJ@Q%%&zmn zbi<_goVtLYquD%{F&P;Z52ecjgs;&NE^dDJvfs-}RlGF8y1%%Y(~-yr&oh^V9km`0 zatb&`cD2oZ%4Mej@*}Qvo#}^;rGXb5M-K(hohFjf z8PL9#fa;}$;hC=<7TBd7xRzwmSmH7RSsv@5;SU5VM9f7yREiD{Z{B`#*)&FAGf{2R z%xZ3(&CUG0q5H>nJ*TfD)H&18o+q8dzSeeYiR%5MJdHv*2>b>5%_L=p8@PLb%NkR~ z3I%U%kmb%S=L`(3i@{~;NesB{EtbKxYH-mH&bM2u>chq_t@Mr$m@=zmx}u`S#kPU& zRRifKd=)l0B2ctJ?GyS1=C9Z^@Ss8W_2>^eOv90p%fc_N_58 zrWKM4tB(&E`C}*@VQYnBPb%wQH^ne`3S`P(b%(Vw3J`H5&_+N|r3X9D+~adCtD9j?7FF zWv`}ju$}xCfZ3gXBc6;g?6fJW^)&o&Q~+n1-Mi)F)SvD@x!V(IDk1q^qTSlqFgs<* zYT7MQyh9OJk3zy)kT-?xnoJw6BNBjW^rxW!z&z%FWUlz z`?HcvI!``r$i(uKGgGaMgOO~OD(?eeCdO_G!7Aln_~(rD$gRr#p4Gt99dWmMDk$3Q zD*_$m;Vk+dg(Sf<&DIxkQQT9960&B7qruVCk10pDnO{&>9wSekD{5Jl4391A8@9hR zIy|trdGg|>`n$AG(=KIY+dxl$7p1RChN~|qJ%D7_IJYJcs^J%2rW;Zzc2 zp=JJs#W_U2ZN5x~wm0NNf`MA|QmE*Wz4;k4k$ec)Vt?QlZ>8SVR~E^Med`F}z$?bjjQHf#WdtV~+u<-RCqGkL8omfgcm(LC-u{xCBTx0R^Z8}( zJ-Wu!s(3fdIMNd4HNf5L)uC7^m{vH$U6z(v9#eNw5L$P^-0S7D6Ixp51dihkoOM^oeo@SRW8F?Sc`Ci(=G!Ma!ls~abCSKfnJ}_dIvwuHF9ItQK7n~PHlnj_ z{-YnKL=S8O;&Wjoq8RgWt^`A-$(zoDVZ<9hk7WMr+5bM?z0=!!GQKSoIGWDIm7(j? zT88Bz0>9XKzna=hlana`PaTQ55`7Co3tnxrX!W7qEgP<6KGE8gaDl|`YM8*NK+2sYbCrgRHr;gP3gv2*BS1%@KD3+)h1|FwUqRwdRH84ssS~g}Z4yaC zNUEURQwuby%pCt3@cbm_;2NvVdM>}$w1mbC|5JfJW=S${dD4L3 z%ddte38{u_22UHm$z0LqAb21qeQDRxhrs1M12|ig1wsBO-i~~^J(YUK%~}2C<`&#X zcTjin>B$m|qi|35^M-?|idJ1{cEYRGMlcZ$JOq9QZ3*ccx>R=7^L0Ve`j(u2LRB0pL9S3qWH zGj${JfN!1i=0gvOk&qJEovGvMBk%+N4P5Gz>KH*Js@W)B(?=%NL64oipHws~PQP%> zs>P}%%l}GH2MzV%Tv%J&(^#6*Ht_WuSS#rlx9>12q*z841A&vonOpaDRL95ePK)h@ z6C#pCkM-J4m++Grzbn$Vpa5Ejo&BXql00o2 zxE>kI%+8`vm4^%-vp)oB{JA_9j^EdXV|4e^z3+AByJ!?+ck5;sR4uLMX18~iOkDTV zhjEYU8yYr8IFVADt)Cf>Sa^R{K~ipVsHYzkgfecbp?I5v)K~&d>3ZaX(|U>_C(sl* zSUaZ9`3O!Q+6;6QQyvM90@-jR@1QLNl`kGpqIVB-(=` zD&YA}-z**D6UtDXOIHapkR6M`GE3{(6H;SQ^z(c@y~}U()qTVefs1fN!ZKdEjT+eJ zy`JczsR4o3u!Kfq)R`{(o*9}MC{2kJwa%3`r|}<++$~;jEBU-QYx=_m$_wTPS%fL3 zVdK3%=eO1zWq(h9R?p+Y`$PhoRWE~~>BWfgvzO!yd02Ixa6n!E7$Z-CC78Em#MPiZ_xve0>&uE~=O8qRtQ%-!z|I z3ok;F1cQ7jMwCbWp931pr>>91bklSg(3Xq{9`)|1p0V{|>klL`WImM6q^m(aaWfP_ z!n>NeIgQ${TGe>B{flO&roMh`&`A1PIM|k125mE)_*$9Or~1NU+3-nEnn6^bBL%^L zQeR3aOIqZciJIrlChwL|3737ZY8)Ry;cKllQpAbwD%Tu&PKAxaVk`_*+c%q{6)Faa`9}}z*4bvl2cr%6=c?0#Uf|++YP5sI1u@i+l}U$RAe$w&ccFZN1#78!CxT_nRvFxlvKR-UgHhx zRM|DUZ>bXfh4|{|{osSSI%!pO)$SPc*DxVpWRxx0znEO$*2d16Z`|YwH)1``LvKSw zBJUDDzkudNcuVz0RnQUM3(X4ArxFc~4jUbmmA-X312$PIs9LBlc_=d}F)^te|67JY z%G(5zn<>JLgRVKL0+|pKH>dL%+(rak!BcegYD43X7*omgGN#{DNsw~KSaOf~+sp4H z#mjj}k{6%x3GB{S)3-v@}nGqusYI^wSgoa<3sfKE;+40Rm#{ub{ z%QV;5RmQnuCPrywT~zgh4Ow%lk!cS6=pTBul+&@NM*U6KZEwF#D2B`}ZeL7dg5H-mn zCZrSTwOeP5D+~9zMq=;AosdupXRw>jU@81w3rcF5SeQ}mdEOO>H66xoc4i6>LzeqK zF{tUu?K4diHOHoIr5OqB>a8qei5+Xe~u_l+_k1aexlt$X!fKb&cYOU&M&fez zyk3YXR0oj|(46Nnc4Yz{scusMC_%j|NP0I-*hcp{h-#StMfoHM$ZwK#@ZWzya!PA(Cj4 zX4M9DP!S{T9^!s9l4B+clOI=_Ix%T!WxK_uTQdXmNQnpA5I5jJ)g^mr!@ZyqP?5FA zAJ01zEArKviMa?wT%U)LI7jlGXj`>; zPw?A(Ow}1Jd|Brz5Cv}I+9Scm-4!eVZe`IfuBO`nbZh=S>;X~3yuC6pzcEJ#X?LR} zcHI}nC=hM-N}2v3s5*YghL-F{v7OFwJHgkO)R#8^?sY@ou-R-x1~iGcX}(e0nqjv1 zJbI@g+qDhISArY=u$mYWBktVn=1eb*1InJjUBAX1azTu88Pg*wrbC?6C(-iLHq$wXXuz=@zwAwIPXT z!(w~c1Is7yy${r64emD$c_$wDT*`FdP8mHo;qhi-NlJPK73Ue0z9p>nr|5t^tX8j( z<%mR4?i}$-jSj_n>+0WJ>FdP#Oy-bfFPV|H4GePd2FLV+ONSc+Y9_hrDJeZkh+voN zX1OoS$AvY2Ro1PdWC!>tJb`Yc<6X6Ta=Xiu`5>=?F19R(ttMVzK3Ms7wfN6#J9hwA zU>GN^GfU1XRDh~fKR#_8ouru3>9yAkR!kwhw$aP-Oo3tTndJp~Jlv=I!SPs4Co3V! z#IP0H38-~|zEJ;QUSphcg8!uVO0F2Z8iAl)c$DV!#m`Z71m$KX6(~tsuPD#=ZcG2V zidh}mt9GUrqnymCRF(>z@N$8V<&c~bFCl0JM)J8dctF#plS2W81|9+Eq=`3h-cGR$ zsf_ao2`9bT$Et1B_l>ONI4?9mMBEJkC&jd${58q_3v-Uh_)ZOoiaI_ZHLesG05 z$2bvk>;qlbK&s>nzunoJu`YTla#%R*6h~7Igvf%o1d#7?@>~x4 zKENwd5~&j!;5mF7Rg#qBO}uR~ZBI9@)~ITem5)v1F+nt6phbnuw%EdEv(ju4Y2rDA z1w_liylub;e?(12>1{vX5BKoQaMYdG$}0sT88M-uW&J_+n6kwJ46{$QO)=h!kVhMC z5-Dfth?MB?if|mERY~1H#EJ0gc`qv=h-3`WVR^X=A&NM?oRk(fTe=@_FNyRTI?$(L z5-Rm5&sSsP%aIeM#cwMTvXP||i!(7ZD|hv=eQ6RV4TJ_leZce=&FroCqjaU5a&yzE z=zc5f4=whG>>r;avJF=hCX6N&Bd%s{WO3-I%^{nqOiK^BCvxKZzTXoI z|7rXG6Y%d{%F4r2)}5!A>vJzgD>74!#7p{ndb4FvQT}ch*bpTsXS%j#X8v}&v zbl%{4!>i}RF)+9#DQo&+8nirC^s=tE7#@DWl#YVuj}eRx)Xr_3;$DEI_WiDh6e8ES zgf4Ax1IJwHRHRaGjC-DFiy%GpM$=vhTxEY-A(`p)ff3WXu_^(`g03GJ+*|2|XjCZi zs~7ox4}&HdZ!6$Q?0q(SuC}_4D;;2{n#|Ae7)t1oU2^4QR=%EtJ^U%Fzi%aiuK(=g zJ`QEr=GdEtFpPs?-I5pHX-2%wNTzI<72F#EEb*@#ohpuA0XP64nxdu+)t!Jke4y-H z%sT@8P8s~4)wI4tJI`yEcAooo9{;=W@7#I+zBZomMSn?2U^%q=3s7oNy%pE^_}i2H zDOl&no+lho`nJ(Lvh!&W7_`9OkInL>ol|)C~wuy5) z5OSaW4uxNdnN`9&6{X?Zn3P(TTG8uvBglBcjp;3mb2*`=3f^Cx`WR^&qj_JlmXl_b zWuWf;W)@{VU&T#DfQPTmfkLOYiZZ<@2V%=JbK(Pu#~$~L){F+>IVcW1dls4z166!s z=bkoacfL%8wVO)lDWn8CQ*IOu`NNo_lD*kuHX@VT0NeaA^R=?lNZv}Ad~^a1Gv0Fb z)tjuj5U(?jXUvUGls{G|FVFQcLr{1mO2-5YZU!k(;m%waI-mXsF?WlDp6 zF|*(~6dTYd?Lr()py7$G6Bjd0=?%Y*NVH`>m;6xaQgXeSRb#>7rH!QKS=!fH_5fyu zO7zFHTNc-IjT;`)g1SZbx84$!yw*xIjmsh7Uj zgve~&okuL36;v;&3*)d{K1J=5RC>*fglXa~-=OOozyr&tifRaK;fM7P+zv5w(e8Q8 z)h(gQj8H6n(-7|#$L%FIvwy5VQM_KBF!J!-$!8NRHa`>&@ad6fQ=_d7ZeP;lOl@== zUSZ=3DIB`sP@sf`GWZEN4bA3u+djJh7S}?*&6BsT>^y(x2?wkHxSda1VLO9vhq?zVwpdTOs66pcKem$Hk50Zv&DSSbjpRU95$F65IPH za6pUw|1U&Y8MDtGS6zoiyLJ|v_A7k!RKIeVJZ{B&T$RRNht%MIHGq<$CPxGsNoVjN zda^YZB*d01KbCap$G!T4;y-%wXn|eAssan2TXNP{y~;qN#zvN;ooFZho7VsJ?D%)( z=QW;dD}L@A+V!V_{Kxp;j!SIi(|JD2lDGM`3^peN12-0l5hPT>Q5^bB+eF3Mli$p} zeMzMaHdJVn^c3U5Qeb6IzSk``)5(Q4*%N+G1pdfB6r=B@&Ph`-P$x4Sq&JeeC8~~6 zpsg(e)ID{C(!AX2HlILmdJEhkxo&p_VuZ1d%O0EeTVeav6+C$*Mv)xftIdTHdNh=o z+kjHdHeeUF=2B4=IZeyFo=vZq_*6Zhfd|*RDfCiVm*e-^@`i%@t@I*NF(SjkoM#i+ zA7fzTgkZ&6W$`h#Z{UtS$!EIDqHXun)bBKJ11d<&N^toui)hgc;N{RGnKLpH=rviB zMCA!JZ>;5Qe<;51tzzaRb?d!}?g6R$v_6yt{a4nN;jwqM2j$UZt`st|_19ivB~Y8w3Jn53?I{MZv> zMF)GFzUEu6_yjmyJsG^geURnXo!gEc!F(QO4!ke)l!6CajP9jxzT8N4n)p4(<3$*zJ z=gQF~(AUP=Bq*B_-KTP}Y&eNufy#mBB%@BC^Pw(bhcQ_!D04~ETrokn#jJOA+rTd5 zU%oh`)F^laJZbqo+I}-jhq~Bzdc(OWipUWl?TQ?=3enyM!Y7dM=hZBqvTKa64N;lBC5vRM`B#-u#c0i|+Ovb8j<(A}QH?cSwNe(>^i6&1& zu^RS7xs{n-X6BHD3f{;vKyn8w6 zL#=2;%|R*mcGwz$ZdgM-eyjCN^1+EWtZ6S^^flGKrgYUOidQg1-a{AK$J0iFo=YzC zm-WA$CyUUHBd9{L2)v)0HRa=Uowf7OKx!fkS1FQ38bY)vrLReXL@P~b(yEV2!Q)jM z#e}h4$PtS|KRY>!DF(v*_`OEf$GMvF3nj@#_j)tnB$}jGL+bPLRsWWavu+Z!XIFLF zN~lE=W+ckJZlkk76~3fg+yVOVs5V?7P2KBJHXSCA+5lB~&^Uh)J!En{0@HpmHu*Th z5H&Zluu4jJ;Lafxr>Qp5}T5dBI9$rl?HQ_ifmS+4Rj;kq zWC$RmvJ`vpQlTr&wuv*(saDqO3XwIZrgP4Wq*~2Y`X@_SSr3qDh!g@zP-6T@igwbwdaXc9z>e%9vWQTk(n~u`tGaN7{J#4$z>au z*apB0ze53!R==KH3bdCVdXcjHZX4K0$7+P_{c^++V57QSEPQD~<(ccwNeEjvc-77$ zlfUA$g4Hk~j|eAP_V)6wbZnh(NW>G7FI&4)uaj)-&Kaj27}|K-4L9yhgTmM?gZs^A z_k2mU4hpix5^~i{NvFkIG4;(`g0n-`=f|>^7iKd_3*IFO@dg4PWJX7<#Z4=6&DkMm zrb*=Tcfn8#vnh~!V|q_iG`|61diNaKrN`K#IKpX&r0)76PiBnp>Or7sH4hS8IcirqR_bmRP;3Ny%JTp#FE?WHA=uN*)Wl~MG} zuv6(5>r1=9<&K^R=>#3NVcSZJO^w5VP$!ce^Qd?x2 zkjWn)!V?ntADK)}Qc;5(pEGB5{`pkXcfF{rk@BlpWp6Q5k9G%DcdrD79NdyVSo`sf zijDhlpgeuAnO?psmbuu{5nQ^e%M;@HXdUJTPvh%hU}n-)&scom%35f{&;(@`kSWt+ zin|vEU+P#KZSq}yl|9Coa}lWUm~>bV;W0tRK#jdA#?Dy|G|gC}SBS*X09YP*ZLvu& zC~3X!^Vu6+l~KI~icwLqqWc(;4MZ*Le3YA8Z(E@OLY|D`;S;UQd*iPd`JGgJ z#-17Bw5WH0!=?Q8__NawkZZL*5cBMO9LzdtBk3puca9+@hLEYmwnx|rDI`|=0m|bD zQ)zk+_(d8sHehP6>M(LP-zO@8$xwT15HyJe&=(5jFts?OS!L zs{iFKezx|a*|3dru<1Ci2EoVeaHRY2cWq*;qVMIVCc?|PYMUSUN?s6%7)qXz`(6Oy zynj%kb{~@egzPNWXzi8a2A{Ct7_-)OeBUV`2v@dx!8}twgUK6pkEhka_hX~`8I^Lk zByKG=JbD*XRr1@4xQ_QcajEZyu8QrxZ6H`)(Djbp)K-0-%qc-DTRsY;P?N&0%JaZULe?VSME6inamAf6lF4vH%w@ zRaE{Emx6-PuBMmKFIOjPM;iv6GHmGHv26|}bMbNOHQ}Xhj_VQu69y1C&xHdczcZ=# zo`ASr4y$>J6FU2m)Smv}DiP;fyK7h1MVJG5j^j3X2Mri8i7e3iK+FiB!i(IaAhYgsVE6K7tRS4MwYgEr1rZ}f6}0X zi<(I&bg=sy=$WFjP45{|wwQCe`Zk>K&~x0db2w~#neOYGw=+d>p7Rp@8tNx|(&LA)>>+f$M1!qlS!$>J{u6RU>5f%scT5Ir$fK|jj9h0UZu>Uz_lPDI{J?5PD$8} zr4IA5Se? zoQwBIayC3Cy)TQDAnttWF&uTTx}q?@V+ozn+N|-U<$15S_aT*Ala#3Xct`UPHRb1(^rJ`o7ZGx6`$+*#%5^)`$vmT*@S~@23unmN0 zw*?v3(gy3*aGkIr{(D|c2rD0NZy|-ng-j`=z~qq6)0jDDJFN}r<5YsCMsJ&O5j{La z7JFZTA%EN6)eGaTF;8IfrFZ!kiWV%im45@BwF&iwTU1gc7PRm6+|HM_=(+@@8*tZ+ z^6)af-34T>yCxy;W@$P}OT?6`#N??VPxhl@Q?)p`DU8Fy+=HG6_ zKk@9Hw!)4ICu_cuxH05Utl^gZHEL+mvVw9y#XO6Nc6HNp`LAi=FM6B(j~kKy`0Z|1 zDylzpbk-R?Ae4P``(YCYVE(PI5DUMVz5%4!b*e7C@TZG6hQ}EGt*E)gSCeP!-o*(I zqA{wCra>LI2VQ?xC?n<5geIvP%AXAj5nF;L0J)&*MtrmsZShkb&k@K>5>`)wH^G?b%pTNbjWdC!9}oK%_Dy zjP8xai;M^m=|@eCdJJ4LxZnzCvnScrugU6qwXo|1;^>*&kt?I=m+?VG1!xS`@RB+d z?}L9NP#jMdef=@#1>hhe0&C4erG`76$g6RNxwSphaKGdpeBANPZh}2!CXD%Ur~5Xm z_HRLF0J-}y=h{MTm&@WuckEz&36%pZcf6Df&hgKqF|PrRzsY(iPye? z*7<=@HhB#wJw_S*;-eGooczm0FNgCP(l^}7Squ0I?ys||#?YyWcF)j;&h~FF2Zd*C zq*QpC6wF+iEdi_&h$YD3j2r2lDtS3}GV@dvxm<#~I%W1Ng*?(nb+tzdv}9=SOQXw} z-0>rQKG`TRIq^mTbr*&mEzfw~D%km=3;lT#dD-gLGA5b2XjY8cW@2 zMww~#$><^NQQ2Lh9t)99nT zjw@Y7Y{4qf-jwN9Z%BqbrBqGKD!Y6yFG8eurZfQ#xeGQWoP5^eI$deBs7x-m!nR4< zDQM0;aubK#RfzJ7$9=7H5c^mcTaZ3CN{Mh#MnuhXK6N!~yP79Vt>D>j-RG@*OrK)# zTkTCOX{LfS4Y`9kD56#eBvE?{*f3pXOb8OFlz2O#nAqGd-(_VCG%jQWo)LEk39IE1 zLEPMM6>dpONu)y;~MEf+zifEl>7x;n&O4;PUsj`KVC&x^B(2b+iopAl!jtweeP zJ{f1E`4(iA|X%#@OhZn|ReGl)$k)@dM{m!C_btSlEK4WuN{ zB%1v!;y*D{If1bes1kF6nDaugcE z3m$&=(&NM06!+}KP`^(Tjc!JHch+}5-DlcyJBEt1ITgP6!cY6VTIo-V6DkD+0CrfN_?Ivnz(cMH~3!O)FPE!i-H`uQiu$V zA~JyrU%VN}S<>buwevL+Wz_Wz^j_5PJ-sL%our@nY#GAII8Bz(bRLZqHjQwC>;RGk z3{~wW!#_>6RmewCyq28%NDX~PJ$@xRe1kfPB@4KKqg6fve#8$w8G+Vf-|1pr{y1?< z*K1;IS@*%T^nz?oso7eA#&!DE z^r`+N)ah2!rr5-)jFyII4qoNqH4a0pp~!sha0EtG;(oA_oRu-v#^a)t8CZ^rBvnj> zjfBIelhieS%{bYvgv`D_bdPf-PDg~w*;X)@q1@${BgSRZEnyvP(tFGiEHI9 zj<@TDP&!#V(Z*SD+&6!Aqs(8axA;hciQx`3K^c`wM>VaZ);pTxvC*LgD~cOm14p0_ zO?#bb0tuQto(~&3V_Gw%iw&tJIm#lveXmNiZhZqSug1HNV^;HceAB(g8!w$W*^!O% z#1j%OC0L6$Am-E3oxR8w4ZAJjun9xEz?}St`Xnin5aTd|ehQ@Qi1DT;QHKpzAa6QNXh@lTz1=O?-c>citAj3d zTprxSf{^e8b}qcp`$@)w7V2q=Nc~R{HtUSu+0vyO>iv5oR=}q2$tZaZHLDbo*4h9w z-wT$ZAe;LbEJVl8_fQ~ zY;vW@)S97}X;H*s2P3pcN_?yLNLl8L3`n&bf+{hICLMn^+D3U#R-g z2|@MzgppKPxjknH12P^}q;H@b7l@*dkS9uD`CT>rz&QHGnDj(W_r}l1z;)1l9{B#~ zhF`Ami0RKw5dm9ERFdx!{2Pc=_rk9)tp7!;qRzPO?8k|NiMX7v*RN~*1=kUZaGpnb zIkw&jfSk#Uyl3_l`NSiT<3h16`!hBTOx-d|@N|Zx_KSDYX^#H`4|vTmybu6u1`~h`dFjCk0f&mU<~M zn4g=WVD!of=y*HW{~jTTamf_;{>Yrd^%zj9-2y%8*VF*pP8!^p(5`euvps+#B$?~o z(1jwqR8An|bBl*}&awUCmILeX{xl*bc3lcg;NiX?yMqN&hTHzP)Ao{*p*u^WuaenTr*ltIFk6Zi%vgU59 zE!*t<0dVv`JXZExoR#=YO6#qJHqA5w-}G_rU0OM=Er_i@qZDi*!tZT0}^c%k>3}S1g0qN*|_%*YK)J-N03HiEA{lYZv4km*#vKC;_A=UzvGYCAMjRp z`RgQrd&U`L%QVw)LgEz_O{*~Z+Mj2t^_>j^xi;;C-!GPWbYTBkAjApCEGVKQx2rn% zDG)Ptn>D_j=g1Mg2xb%(c1%~hgzcrkqhKn9BbWgLJ6)&KuAfQ>nFS%}0Syn_wREtW zoer;RsjRSUOQ)Z-G`x3zIt?D-T&$H=w&*^2g~S-k8hfKM?dAB!IMiIK7*??4-W30e z8NAs9U?Bb86xQA?*D{9?7aI|;G=6g0QBv7GnqZAG0%yNcU{LjmIS6Y5c%ESjvs~w0 zL#^+#@nZdulsQ$SMrLt--tsRAc|r@wUII0~SSxdbyPttdNq^VqrW|0g8GrScO1?wo zh?L0OggCmPVwT9D?hBigD`U6}GmJ@Q-N+Enpp%GCjrU()!a8Pij$j^wl^mjqTyvFQ z!In-pEh>A-pmS+5tNoqmrIYxnsn=FS%jPty;UHo`lJ5J#>0PG56(t0>0+4!h^N#G? z;+y4<9gje6<|nvFTe=e7Ch8k#>Tx?ko(Z*buJnYtHCh{0MDgO%>j`mpt=mOiz}e7h z9IYJf$e?K5!jL}XDY^^yq}5F1-aAbzv%i0&a$6Qa9R0xo{QN9G$9FSV_b)#d`2NM* znSl_EA!Hy)UriYg>;vKxc2g#wNxCdDaGvsF3ZQhnpcV`1K?0ygMpKq%W z?iQ*o4U5Zx;|NamE=rBqj?%lm7RiMeXn2UD;QPnfD=DFO+lwiAPV*#^c?zi)%F1_kX#v07iL)Tfpd{rH3o7vn6MkF+^}4RYh7N zsC=4PJVjvXWXovYT^;&f62woc1>@|uq$PL5qT9%d(p++R&x)$x(fq#ZmzVpULn)Ss zz5B!3CE9U+0oLjZJ7q z)>dVs?H-+@iQ=bA1kus%(QcZtBUN3f#uUk9KJBt;i;)?X*3yWyi~fWO#Ji}){=}n) zkBb4hVQ`%YO+pd#aJwd`Wl}cn4Ki-73wCovl*(<2j~E>MU@s?|Ml(o9YB+%9m1}@3 z@lH7?h;uP?H}~S-4qtsdRzzuBetV84l33fdf=AWo%hbo)XH`pf`?y&uh5kO?Vw~D- zh4;gczPKBp*kVD{qEIp+@sz06uWVN2-E`09*A0i=&nBB_sc8+Vt+`tkWFTSbvoP={ zOINqa{@=*MhvLw~!*c=)c6JR~>Ko$DkwkCF5|!9+(gyTEGK$}nAR6BPZmu*W-FYnG z@FfgWu5774?1M4w?6Xk$LyYcrRmEQf^#DF|oeT!{KQ~nH#nq3KlJh0D zzefPUz+y#xcpksTP{&q;yhEYbZ*uZa`#P)N6{Xby1KpHAgT166b4Zeg*w?+}u6&d3 z1=HUj{YdCnvUO3r?K`TJ1Xv{^hnroCE9X)rH!?^;cW$H9iRF>7TrUNqBYAN3xVAiS!66akv z3mDYM{946}@cZ&}oJRiF+NwHZk}CaimUvHF9}sMN$kDzr9zPWoR3v!YCBt+9zjUzb zq4ilER~V%4aFDw!`v)`XCkbG&f4IhM{FbHaqy>ACs+adWpQ!AwIsB*ZdYcD)crfuL zc825p|5S90z0HY?hSDlt;E_jq1~JGU^&n-W5>qf{Jvf_Z9mV(}_zffxG%L0Zai@bS z+T?X+Y-fR_UB|^J*{YY!++KzOOyD))WZsx7&h$TL9^?JM?D36>*{K;Zo6Ei8PMlLY z3SaiIYT*2DMzfw1ctJS|F2FPQvEW~UW(|D<1+(5)jQ67rZnM?GGJnu}9x+Ql+oaTI z=+U97J$k@&=~rM9%YL3U$rJ~%=EA{jL|~Q);xs=-d<*&q&?oQBR{DyAKqm_G2J|U! z%oX?53@=HpHdx7NU0_?NnhgZO;I91P9L|Hx#3ae5`oy(Z4e*RmDpw>2{VdCB&z G<6i(Is}hL- literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png b/web-player/design_handoff_musify_player/screenshots/03-home-mixes-artistas.png new file mode 100644 index 0000000000000000000000000000000000000000..f7a6ecfd07017b5d1e03d56b613028a913161691 GIT binary patch literal 32697 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 BMyLP) literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png b/web-player/design_handoff_musify_player/screenshots/04-home-populares-playlists.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad339932f1aeae51a73bfdbad3e871bde3f218f GIT binary patch literal 32691 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 GKK~o0cS(i- literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/05-descubrir.png b/web-player/design_handoff_musify_player/screenshots/05-descubrir.png new file mode 100644 index 0000000000000000000000000000000000000000..a5f9424270690cf63217c07f1c9efee21b3ec239 GIT binary patch literal 34053 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 CEJWS_ literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/06-playlists.png b/web-player/design_handoff_musify_player/screenshots/06-playlists.png new file mode 100644 index 0000000000000000000000000000000000000000..37e8b8c804db26bbf41c4ab815829b0204eece16 GIT binary patch literal 37419 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 literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png b/web-player/design_handoff_musify_player/screenshots/07-tracklist-me-gusta.png new file mode 100644 index 0000000000000000000000000000000000000000..125a45888f26633c2fb20ab3bbf3d412224d3c30 GIT binary patch literal 32766 zcmeFY2UL?ww=n!r1VvFnkSd~5L+{;!0w$OMA%qT60t86t9TWvDG!+6URZ0i}0@6Du z2ntAV(mMhQNJmlp0_r*Ez4v|Zz3;#NyT0|Ub%w>vo|(P(?AfztKlAK)^5gxF3E(6| z9jp#ePyhf0`49N<@$^H8HYEeFUk6U#w)enTl3^}#X>BFxYymI;Cj$W>8<0;kQm~S@wzRN8 z$&tsR!u?_=_>-N5+kYBV)WXdTSQ8d=p6=jV^ zVckVNrS@r9tt8#-^l}pDb8g>+e1lHG?j5G5BDZ6#$n@!GYb!+?Cs&MxyFD6XVT%&8 zcShOTqp@!87Fa73_?ow{H^x~JjkZ>lkQ28dPsLh9!bV1tJSl5Q5%TP;MC2q;)^aEX zc`I3Il%p%k(c(Z_JG8T%vxO^J3@sb7hNpfBe&t^aZeevmDC#Nsht#J1*L#9`ui78; zZ`dsms<}XB^lV@1G}~XuchhWsLJkLWcXCEyQP%e4aV;7e^aJ!Rc<>Bh+ zE#ii9w7IY^kB!O!)t@RB`_CG*wzF`vGvHp~NW8)wm#gh*pCOgC@iKWCtwB zgS(TL%uN*I=49jUMP9!vw&Z%MHI}TOTP@QK8-Q4@za&-al|ro6l$BQw$O2U24XR+>D2EZT;w7JHO8 zS!TEW4Rt4tT(@v_XR>ibVQt;**iMH50OiGB^WZ<2hoqI`U$@wSBiLV5H0GzBQl9va z;-cNqwpa^y4_A~o^e>inU|M4TmWTLhjeoHFr<+niRvu+zB}Mi$k|-Qk2|3qaOK$$Bp$GjhZiSz$L4TS1Kiy@2ZueiSdBX5im_HA@&Xp`v z*(-;f(-6m%`)9eTWMNqnU$*#?b$;>{{O|bwVPwD9Dt=)slwaT6^KzhBE6~?jNi!;q zlWBXY9#1<67uNL8az9v2bF!JETrJ$4T+Pu~8>jRG({Z};dmnSFlQ)=~ttZ)~kSzx# zAu28^?%?K(vO@l9krb5{6$gNm^1rM{;FtCM6OaDKf|Bj|>6YsiI%~^=zS5{3QnA%L1&Gtp90uLH-mXGTMI{cF zU=e4gq0*zKJ9YdId4;*PRj-nz6yEQkI>q}(2lv&VJDmF}`5{5x*=P$#H*UC-lRNj- zE8<)%^pdjT^5Sw5^3qZ=vPR^pJh>_>p>R+YzxJ<(5)|kEITZI_hWhELevhb2-WOzV zEN&=%iiY+;d`gO6^Pr-jrBVf`pZd}{v}&^+V(OEwQN=45Ut4%TI#Bop@`nflTc>Q`C20*2Wih zreAl&Urc;8U{e2AjZ@hI;?|Vn5^iUhk|}(oK8&xlspH5xrZ{Ybds zk)mYYL+^RruLzyV8jT~V`<~B`KKW0-_c!#n2L9H--x~N^1AlAaZw>sdfxk8Iw+8;! zz~379TLb_1G_YlGS-DD$txsuDF2HCtoBBIHZvBf&)fjLEEB2_Tdl$#awijXI{k`tY zshiguV;jRo`osIF>ZBiRzW0h6d24Wp74>6^;yw84Rda2)mOA)`+RqsE+x;m~lVb`P zth*~*LzUaW(1@FM;%D%Bzt6(T&3V847l|Cy-s;~EZ394$$iKk(ubPkSM;*xspUM9` zuH?bV!B-|S%w+o$7TAX^f5OW9u$Q~DI~hlNA9h8+uaaSFGAv;G7ufPIu$8mxK7KeE zM;YVju|L*++L!nk3ah6}E)SFcIRQ5S4rl#FPe|J&)uOMu}pB_~xP6~%dgl7WJXf#Sz&fP<`VN~--kkv|dz6(#i{ z8d`GajgtU51Vu$fbL7Zj3MwjUD)Qn`QZZ07o;xI=bBgJTh07C~^KjieQ3YL}nNLef zNh4BUO8HS(+9 z4o0>&LGFui%4om_YRw|K?SG?RP{a|ySK38EE^QneX>^5Ti$;4LX(dg`7v2~vZ3wV>CZKpW?0+0-?Q zXWJQr6K&!hd~I!0dNaEdFrR%#*~{N(G2!J+5usH`;oR9sXCRyL>tWsvibJ?8Wd1F6 z+@kTV>t&WjH4`(v;t3E~ZWjLVUN_ycVvV*2BMF2<#gY_sWAf2znKymum_9#kwsK(& zjd59R=n794ETgR%b&~O{Wua&HX5TR~33d_3G)P6kK0dPJeC#W1#$r2}(`uW0$hU-9 zc%+1HR%!T_X}{#0@g@4O`cGenU7e7@-9u-j)4p}MgP@>Q3>h99|( z!v}HV_R6zYSp)=He4LnXB?oCZIDX@lwQRJofF-8u^TdD(i7X2a-kc{79P_>#+fRpn z7J#98P`>x8-0uvxUt1yR5=Rg?QVnTjVzRVphv!QcGBH);i{2wck;8P7z|h4h3)O5FT|=gf7y|zLA>jCZzI>Q2AA=M*ZHz+)*yJQH8mEc=$cITG+B3!VVzG33HCP4b= zHug3?d*Mw~24T8o4-}_0VF$=M^gQC11M{WpjOK;lYiM2o@>`OH?f2g!$x9mePaGKl z9JSgw4?fv{H0!bAdr*wUU3l;+9Yd9W@ySa@b~hT3 z%ACqpYcmtf$xAN?tDknjfTx_h?W0#yO(C7(m^bM!royU>mPaV7c1#p;;-Sf>V;ohk zk24rSmcX=wUh6; zd)GNU5nc!-xz8(1P$2datFGOOT4MBy_BW(HlWlyKaR$#jRpQ<=FKm3eM2kyQL_`_Q z`l;Ie160@J%v5by0IgXBx|yG@ps4GOo4U}WjadcX0sNhO^%5BYMK9#+IZ_Q@sg#Xg zT8@5_K5tAJU&%mf66k$4PIp{jWO^we2UN^;M#cJ@1Ec2NnJQQ3m$Vu;<2tX1JwGh_ z!dQd3Xk_ekK7aCHHDU$ZYab|=y%g?&)y3uCLbCBsshKIudBkrP=;d^$Lvq(uLt777 z=Z0QBb2d&ks9{aHE-X%DEw_I%U@CHCh{;YJC$v+540C}tdMI&D(9E96iAX)Hn&g>& zq=Iy6HF^71(lm34gm)z@#{OZCYg?>1*gWz<&#!%YQ2gGs09y}9Q70;>W%iq<53{{u zQ3p?v%9iohcVw(;-_v z%M8w91{VixJ!a2&G_nFxl~W-DxNv}N)J64U=ze?S(L8}%YQvu;1!@3C?hk!=+H}8P z2q3pJ{G?lZTsgH*=X2fEMI1m0nyZz+ro03Eb~S&U0U5qwJ+&b<%M!g46DT~(61DqA zpgL|02^8VXG*>v&*35kjDBE>>#HwAxngeEUf)3z^&=Tw1@XO>w7n^+X)TaS(g(iA{ z;4tJlg{I`47W?R%*WZW;3yr<@^}Jmvd6xn_(Vu}sRPz)Y08p630v9!$W{NkHX%RY< zb3&*?IsI5<`}VhB0UPMrW3Y0WSKl^;wn!wGmMmJG4HefkrJ5?#25zD|-qyU}O83f1 zE>0lTd#7blzw7xt93HtK|jOhZiXd+RGpGz6YJ>7ajfM}#0{ir+K zEbGrlDOr053fwiTM12=P+&w}@a?~aqc~v1)6+gGthDe-lEhWya z##0CjTSmvoMvshc$_S~cr|TfEZ@mA6JjxK_9HGSy`dn>qHzqDsVrmpqr=`6HMfCB`CS!^Ty=v- z)>;g4B(DWEfd0Wiu)Qu=GdkF)ro`()Y>0flG%h}Ty-Y;31z>3>$*{TlB;b0JQ4S19 zgw3o>igHOSK6*(Ia8#|nfRyx!LafG!P)TWutfI9xXN?VC&APb@#G-q;Z6Y3M;LsHqFxv5@vLf3iAe zwL86X_QG`ao9#DazR&-MZD}-*MAp=m-re>On4Ssudb+o>+X1I}B=k?({xjYcjkqrt z&dgn^y|}stliTOuN!>heBrUS5?m5Sg&@By$*AXKE`r>gjc3x9#%O?+Bv6bog4pgk9xL)Vm2XG~ zLs}dTv8lxX>zc9l|)tYjb11kH8GVeW2b} zP|FFvd8|U=gLbQMhihr3$9zk0FHh2^Kn$ghM_Mr251D_h%gm!(y9MZl?D6HVf>&9IXkmiKVPDm;wPV*ik_#`GD{E#RLu9;U>cJ?C{G%VQwnHmzH`hWO$ze3H zlV*#NA@gGo5rioh64m3`G(8_34zd(%(jh3+cg6|C&sVnf+Xc#<6V*6%-#fH`$f`6X z*%l2KYRa|H>0mfQgly@yCFDcLqO~cyW-U5EvqaVwcbtXNd{Ug21_YG-MVS+gAhPw& zO7XG@qL5SN@i&jx_^Z)XFU^H!rN)Y=@a2&fG`J6@{6EWRjc*1Nra0F1&Lm;(Y_$Xw z{s6R9_{2VW`~c#_ET#(1nshjX?Vh4wDthi?y&vZWrp z=4T*@F+E-m1tuGj4`034lpT0pxBLS@`fgtvXYb$_=|;!s;r(yS_SW1-x+{i)bgG<+ z-lOAW>fiMaN%u4;sH~#XiHe@mo>8TAg!G}X+ef}maumyAOki9|CQ5po*{-g4jb3fY z^`g(5c@iFZ+&gj8R7R08920=*s_J&m>)Ff7zmo&Q>#0lS!=j6`>raS$^$_wlQN_1Z zGV3XsU(|5$Osa{Cd^P#vz5h;l9>^raW^Tx-SyR(9rfbOE3LcI!GYNM#NldqO+!}Ry zm!N+kA@rqQLqCqugNd6Us4m(CHHXJP_1#Ei>rIEBjO)UsbqCz{VkvRvgX*!#Mv+L^chTm&4B2O#bmhJqR&09d@I+KaiwGaW zOE&Xjgo%tDJRg!q(gCZfXQKK?lH%wY8iX~)|9Yx8Ox(CI^aD75{Z|?K@{lzEs304^ z4`}R}f9e4FlEQZL9(}7DjLLNiJa+JGr8vZRnC$ZR*#rhs1Jb2$mWReye*o6lcqQ{c z%$=L^IYq(USC$f+eVpV^GTlFGhTU`s>ec-$nRC~}<#Bkq`f<(L`45;&v@QvX2{}{! zX=N=O1NKype_9tsO`)vY(9?EoHpomUI$D3Um*#<7X>Gfo82jhtw45oTATQ>-`h)*u zibr3R+rMvFO}~H7H)?xHRZ-X=CH4pS}Rc2RrJeQ%h* z|M&wCHEU=jKh={5Shf^@0IhK^Rt+BI-9#9l$T7`d`(@XRxgN?KhNW|Cw@3GKxBVYZ z`m^Dz%053!igq=NJN;zC_IiPks=IeO(tR_g@=Rn#dMBFK2Wi6p<@};TpP>2kIZUXb zXMq0TlEy7<@S@Asy3SyU?jAo=RTpVhY9LyQ0KprtIpavRtuA+pU=k#CruC-N39eO| z0VTb2^8+{&>uH`I;~ws@9O|BoSz4rc=({2C$IfwGb%)3BsL;%Q+XRE<#kv_GBYy?lHH$!57tYr7 z??p6~khSI%`^EnjtiP@(kX1DT61uU_qUjkrqZ$KlZW51n@YQs2ywGCd+N2GIZQ;7- zKZ=k9`h!9g-bmUnRoQkAR8i^m#N*N?d8YFE%FeOt@jJeVuFZZs9HOVjLtv4IqpL<$ zygZ)CK@d1JOh`t-NHPkEA<0l_Jsa^NqR*7=bE+ZRRvu7a_ieBd`fd z7d8@XZ{?*+Ps8wr3iW+_i5Q-Zw%eE4LqiNTBnf=w?;#Ne4a&r3jfi4ddsLFZAUvkX z0rXHNXTCy`2Yu1v9TQ(zX?oLsjTs?T>DpSmB_!u%IqC&Jo-|yG@+>#Lt&>eG z%XZP9=AofRd}Ua={e(CDQy>+9PQIL|gtIvGA#b79Wmt5>)Gro_@u} zxviw&kDtgt2hIH!O?cnG^7e$_!(rBMP&6VIl#AU6=bMVN)bO(M27@j5HSGIb9j-Sw zHa9zaDi=?J+;AYZCLK@o%8kWOra`(6s(20kCxkOhiDJ*TpGyvUi#!W!BA;7NHERde z@Xe(@GhN*{W4EJJP~dg%6#J}>&=9lL#SPseWzPEYDWPdlWicA2W9X0Q^(r+jaN%J0 zgPnYNGe?(Xi*AASmmPa|{33_d?ajWT0@tX>T50LZk{*4<)>mx_!6XH%0vJ;w2_lu1 zHdIQg?f#_k1Mr>v0mOWfdNzqm80rl31%X<-Ushqg6|U0X^{zfPGtHr(R+Zy&Bi+JV zxnRSmU(}Uwe~PBD^AOxoC~{`Hplfd_#^2;) zbt^9xW9j;BxqhzX3$4Ry@3c3n#h-X=VCIWV^u6zwM;g7Qf*LK70;0{O<|7@u0`%u? zFWvlPqy)A^^66QCWWiVPc~#4m5i68FH+D$hV$uf#VE0}(uhsT9J65NSE|jxBHK#4r z6UIqAjZrpvJ_{5$gJGu+~JDLuT zpSH>rBUWA>zjfl(J3fvhoUj2>)YqA|7*Rs^xJAruT4&Zz!a0ti+c+-`S-Y}^P+N_9 zwomh_k6PSwVD#0`I0^%N)3s1bBGDl+TG21wzj-FSDDL)YfhwUumRh3N-qNLn7ZT2a zG*Xcpt$90!FYx5+4xcPqmD0OeUT0;LS1)diW%Zd_m~xIuK8`mezx{lb^UVws-w~tT z!p&+?DRh$@cA}zgM+W+sAi2D6Kq1T`$%%zkXCi_owp*mf_)jV5HjH)YjElJ{_-P(d zKCYO@l~GiF3E$|V;@YvU;C36|_Vv=)cl3%0r!1!u((g72OkD~Ag@S84T=1oy%=Aei z^}WH1l`%HyGJNqy4w&Pz4A80hHT{kxRO#-><{ryJ$gA;={F|Klhh82zzT9zDlf$3=EG!Nd z8D7G6j~|hZ;WTR_z*zlnu*ci$p=XU#!0$d=ZO@(EGi#zv9rivG{h;Z?8!LYG(rbl8 zUIJd~QGx&2$cv|5PuVMa8&L4n_N@h=IQ8;_tPZG ziq^kT(0Mf_VkG2mZ@R6r7xZq4lioBbESak$A(BYyHa)g-bJdUX9w_OCC0tIRZD(TgCp*v17EE1OJTMC!!*XF5o4j~w6Y`Rp0lG06BDf(Pq|N7;WA>K`WE zevP3I3aS4j6G?yCZNk{wn%BMPRyFK{^QDmtxRc2+(! zh>#`OcfN*YAkN3X?OynJqv+@hq!7kZ{4r6W^Z7Tl3d+f+Yy#r{foUo8`p1Ry$(=bx z)fe8tq(Ml$!fgEADich#&merY$?}T~U8M91QlgDZF>QtZlDGDr{@lAQgQ7Y-h zMX=+d5C#lS4f_avN`kQv+mT!vnHCpQ<_pqQaQR&!t2d`gK{s6@ooA+nX6(0-Q;{n{ z?;~RL+2Be_N01RW;#*uXGPn9If_?xiCg$H$aF_E^f~usB%B|fK5=u00{pRE_(muQR`2 zAatWVN@<4$w_3kn-gx?#S5lR{zrI(z%ql&rzkpJAj*93Rd0Sx34{{5i7(ubP4_sBb zN7Y~l0?B_<9=(?P0Wv0+T4MEP)@gpHHOo3KYWME+a7Tkkf=I7~S;p-(8H-66omBRy zuttPX{%K_;E#4?I&1E*;nc7L>BYoZa@KahM?Ass113u8AmlP9J?N^&&m(=CH8RGxI z%L%^{3-%E!-)H_ng2r&4cE6MABSn#MI7)pTQRUx_rb{ya(~ zMeG*tALf${HaX7zK1z%MY>2!2M~Um)WLcYrA3I5yq!)RfYWyTvvbej=?_zC{rqdF;lGa)FjSKRPx;n+(ux)>iDK~)YUhG(X z!>bQPqlzO%1OZdt*p+Up7uDw?DZ}cgCPBDMAtV#0E-cO&;A&EGvbgD{_o}l7NwU*z z*&UmHsNq<#G>;*>!s*lB4D7p$z(QMzk7;ckm!3{-C2`u&BQh7%c$0XRQ*>>OxELI3 zQdPx=p8CO*E#l=C7c?FNzVGyd%Z2&a5wg&ua+QXzjUK~(Hur;e?eD$werTDFbj|Lm zX7qRRD|tN$$O;Wa+z5cIYVl-OWNZ&sB|SPUJI7&fVb{}8U(9=C4vQ9z%Cx0P0e2?p z1~)27L6S*F$3zv&zshq0v@iUuEIUc;PdSBA4-yKa2ysFIbx%_6)!l6P0m!Y{MiMT< zGWZ>4(eFQEl4?Xqs!4(teZo;~9!ley&Q$q#KgUr$c%^=?hcL~9%Rkl?Q>i9KC(i1d z7sDxY-6H)gv+iAeuW^U;w}4`mm}7^{^dd6cWWF!DgEqZndn>5|qebh0_@F0nYuFDK zU3IJBO`zhgA4Vky&Oo4rg{WAu)YJ-pm?+SFWhH1Sm@x~}FIrd>0Q~5Z865qM;EfZr zpohk9@NC8fiA!OV)Xoyg9H*eY_044*B38G;oaKQN7WAZvJvvzJ>5z}^0zK%w?<~*P zjkV6|qOJHJWJlFL3Eg9U*$a5c@(mIT9W)zf1v(b$w>Hu%#14C6jIuL1Y+y9uocUtRS2$O?UbEhDkAnZ(3OUdr=g&tJ*tx_yfYwX6i*^}nqmP(8 zeL9&^UL}<_mZ1e7CynBw=QJgj6euCHV?5Ije`nQj{SF0G{QY?c%N4r_K@AZ30FzO9 zfw=&7JGalbPQE!(vT6_lb8WEX^rtf=YCht`xI@7+03r^r?Ib4lB4o^fb=!SQHLUCNG7v^-{6!&hCoLh!#+shdm zdqVvl36l*kprTL~WBr2FD3tMbaq`vk;(K*e_v5uJ@%uIMzg}Jb?CEI1n$Bt{#xM>L1XBLB;8P{QG zu))iwEM4F$TrK@9^Qt%tuD7Tg>zS4SmFdiXna7ka-vn8JWSV{l;Pmn3dN3K@XT|X? zJ8Ar?5=0}xDqVf^K=z4R1Y-jz&S$%%cbgU#Bg5M3_HNoQ$I#r%j)`(dyPaq_*bcpz z!#E8iDS&+9`0}TX5j5qp{S3+x;PP@a6Wx+FM8ZfOHD!yoZinAafvdWKuMSbJMUGX? zwD=JBdxAPA{<be?V7=<)_=|K5OMW8QxnjDeDemmBGx*N|n zssJX;QyGxs?DAJ2r`FQ)suVB3WXYAntzF4&79s@f1sw-x*|>&cOx0HG=w98H!{KF4 z=d)BHzlXRHqD5vB?6*vToBPY-Yx?*H@YUn2R!OqmMgsq~hnQ9gIm%^X(lj-vC)b;R zYr*J+);_xZ=6T@+%EgT%>`qMMJFKWq%-af<^Ajj%D;H-;?y#^sa@>LBTOmgDlKJRn z09P-DJwbmvz+9t*$)}UTkbY5@D6C?{7<0Wt)S)a>uL~P=tzaN~VGWxx!t!~}bD7HC zm876OGqc_jkw{k4SKni(Xy$OdmJ|-I?Y8hFpRVs_PYAdXZ`~W;bLUhJJOh`FzzvPe ztlX5na2QEs=9Ib%_gWpzjoGZ(6p5ibqMmoXMKlp_S0PZEVwRavz>F7Y(aToQm@ngZ zzxJ^&Yelgrx}+rfCI%A0+T+nDl>5X#+17g4{u_ocr3MlwP0hv#v}7zTv8>v6rKGNQ94rTDK07wdC1O^M*sWpmzF$nVHmP;k7Vu znV-;ReDrH%LRgYv^Ba$u(Qw2}6a>n%Tr#qAr>favIQSc~+OdTWt0=-zS%-=6-H0Nw z<~|OOLKEe<#$+X8&lhJBKt*nm3kpijh#$|&mgk;AyXl9vVcwrF`Ze3x{Ft6X# zm+l2_AVOb8PQFTAGTvylZ-af&%@89fT$Q`cdZ+7kMRz10Viv79CKWb6KiY304|@7e zF*ol6>slDTZco_0zQh39&dkqNvj+FFQTUK@5p7$uZmuoI{bD^rP+6s@7*yXZ zV+}E>Aw9Kd9fVd;GFOS<737JEMtT`7Tl6L%-D1kkyAM6hQfEFtQP|`RFIjO&i!wbdExjl6cPav8Z44zHozNpg;bvDl^?tBg3KR;HOK^2-<;sF}*H%KWC!S>ek- zSJi?`Jpuu>K<%Ggr))IR(bg$^JAC7rK2|c^;;_wEnBXD;sFaU?9Me~FPrW=XV`yY( zzB7=BeiwUQ8;xzpd3pxELRx> zZXdH=x9Q(~XKGyE&VUgTR2pRt6f+>2ElXgRNMFsn^%N9}$EmCKZ_Ua6rt4GwXm8EM zexHUe$BxnB6sP@%O=lCFr?9bV`I^3B-g$Vwa0xuLPwC8IFpLg7D(zaNp;RKhG2;HB z_9FZ7{vH>*Vg_shw(tnGac-E$9qzEB`YK5%g1aQskk*+saw|bMtegN-A z?OWk#YVVLlab`IgS;Bh|`EfP$8@SLBNEf&-*T0=8e&6&d@c+J|IRXC5O^npg)ZQEO zFRoi$LgcGI@*ostMb1uV!Xh{at!lOIs(6Sb1g0N46oMCWZvko5#Jco5339P=tv{Q% z_2K>=F5tDB@~I!d=g&WY?nmV{Ag@P~ zNr``)v{?Ojt4{(4ITilc8T2InLAbx#*w^-3Vmm7Qz8ol3x}(RUApN@EkDpc`Lctm! zOmW<0_UB|Sw0=}Z;)A%flt(!wl~Pi7J)B=8neZ~Rj*IjsrJ7bsMcCN{RgRW-4e2I= z)YOqT-oI+oxr>)?hQK~T>FHKeLTCNV7H6V+44Tdsdm6o;w^-Cozni%T*6{sin8P{* z-DK{AX`VM#WRc!L-J1N{?<#Xi5O@UM8a>VF z!i-3FCt+@!Q|JjI#BXd5y?#}px|WfE^qse9w=R&8RJ&5ndn@UKD#=^@z97>~LSg64 zdWH2Xf*+hx&nFQ*?qtW8uGkAjU>u{v=It;FE%LndU!pLzT|wll9IsNbpl@68uPyNc zI=9s&#zkv0oV7CH1a?F!jIJvf0bjiA78g_{B35LckBh7J9~-<)zD(q)`Jm0@y65Cr zSNQ{oGFx5uLwz6k7PXypnO1CWt93X33-8KVxl;4d+s$G=y}i3^BYS7B|2H@Ize6A}@b8UG)ouBg9s7@$_0NGbk7)F^LbPTt*{i?_ zy&cSjtCICoyl}>rH5osT^rADQ2eDt89D+I*ns#Pee3#c8a$}&zZt_t>CGivFP)<*I zo}X&<6b{8z=C_CHedCd}sw~?1ee&U+2yx3y>5SF)80_}BZtimzAvoRm zgp6+=Di>GMz1@=zi)LR=oMx9wg&B%NGHeFI$${2}tl7>X@}*|@IXZ=O54y;s(oV19 z<7+N&c);@`@CuVm6Q7%3)hA;dOImNUi5`8}IOHC4EWpTKJQRvPI*1;qdeN_6S-SUp z!*_-9PX{8*SUc@-91nsBB6M865>eL03leFB=8mW^U9_>ta^L2+z_gA#o-7~FS~w&U z`;u<7p&$z?>jg;Wu^gvBt~8Dd$FpXWMl~IHzG}%8b1&%43|EE6FRP?ZZ`BQhZcExhOzn6AbUo)h!NwM6$0f#Z})L}h%8!;NQ7r( zGo(k4;iGe)B zAJL1?z*BEHk+#kVPEP7Fz={*vpkUt4mAdy1qy!kl{R>7ULDfacFnt~D7Z2mWJPx?t zC~ruEVjf=kYnz&olJr<6VkWR`jz2s#xE?EdD#EujTwq3zDLUF~ zq0&n%^$uPYF-t2{e_XL@YbW4H7|&0R2eioh7Pitoiy5aGP(|r^hWI|lD$dS?Gh$nh(}T-2U))75c%Hmc5YmvnoI&7c zcb|@jav0c6;_tnl({-Qb?2ySzp2e=49}SP?N`kmyI}EhM6zeBNrC-vPm%sMdrZP< z8{Da`MU-F((*Rz^GRrd64+>UWX^DBqYpQbEv6{`357|HzGHYow6^fVo>bhhD&+!{H z$ZaECuRFZf?__#Tq`kvU3M(V1U<8kjy6eF=S{DC2bYANO?5H%+SQMjdofKzW_u_#r z9^@pG%4Y3@T?%loyYd_uOv{Y1>w(GS<;L#1<9ALAC}g|5<_FbE=|uPC*rmYMuTxaP z!cl?>ovqR_#GQ^FhmrQ!(}~$&SInyp(GT7W!-R|vfwBZT|HL#RUZ5v@-E_1@Z0n>du4j{h4?r#*6&NN!A%^$_p|P)RPc0^u7r+_0Zj)j8hlivJKV8f9}+$56hvRXu+Iq3m)aplr@KKAcm4O`fj28sA*9SL4V)tZa4CtWw>s zPFN!{swibEH67hfch@yDx40v~5RdekboOBl-(_ulS4{L)ZzadS_8H!OWLsNR2a zD8=^&z!}i+;dt-vz|QnCyUKfI{`ND(?flg@BNv^Qv}$fgUYz`2!yRdN3Faj5vf@35 z3pMTzd(SV?rz^hrT3mRQB$|RoilL_^3R)k$hPkYHiO$%cgpB83w;z7XD;pRHv+Wy^ zx|lb?9hX+7b^YD0(tfD!ze@k^`RQNrqWmV-f3g1Ve*RK6!r%O}FEq%U~)?Ioc7^KRnQC1TOp0& z+zj4(rwl?QUQhaT$c5H=4_~yMFXuAhy3JZE_ zKsq1ngQXw}Ms97SN{%u>9@ubxZLzMGM5~#o#gj1&KUvvIN{lw%PJj3a#~(og=>*@L zO$(|HIR2L2`L{8k*xll>5*U%CbF2_-SC~`QG0g)fJFT9PphMv6De5dbir2pdVAyx+ zCN>giYRNCP+z!8}&9#mgWgyaNZ9WwTOlTiLXZ9fN0?B?t&sw2ms414w!esIsFIazMzDQ*Pag1<8Hz7PG z{y)5C{~Ge>iw#plNXw!`O}=Fb%|gY3bvM;XQ&+sh~kjEF!|0qp?HJT7sjX5XmslYBTa>Kp_If z1z)<=Hn{!1C6fy=%^xq>>e#HJ&2+a}C(*7cr_@l%gL4Ay6aomop z?2JGItzq-fXNT^tX^FEYZD0C`(zrzk7z-V1x_9B@Q)U2I+7aU1g`}1I04%>3sHpfW zCQkCQeh46P5v$cd6NaJDPnt^MR8R`1^v8!m!AiL6V(<21M$ECHR?mb}t zn5^*A2GIMH8xj5Yx<+spNO?OT`cRHSFR$%pd?l45= zjNqK16%(mNpuA)?;Mhdgnp^*INjSRsjpXFO=uK&`3!1P0k(=x+k|Dg;N~1B{b?4zn z{H~_;q+K@l-i;N>Xd@B%y~}PxXucls@<;CBHV9K9F-rl}BMJS^-}yEC2Vk49WfEz! zchLd6SVwSX=Wg+FYZdFNO}fWT{t1Up%R*M0u5%Tp!Z4kkY_ft)5+y_LVm3#I;K2Jf z`xsF38^y1JN*}*6gnWJTUfX;>r|HTM+dBb%;V|_B2bue06N`#5n=7Z>B!BbXBEvZ~ zGE$95nPXajC0@{J7iH&XO6s~yrP!xH{?4~>Bu*#rfaibA9Cco+M<@x{#5z=MVma}r zv(-L?qm)~^v>uro<^jX04k-kq_`Mw$Cvw1xQ6_bC#X|H3rE}Xp=WI|EUjsX=$ z^x|cBcb@j=F4nW<%_Ly9aQ}QL^3cMmGO%KPp+Ti8ww8=@TD-#TYj7-NI27PucF@Jy zm)o@IW%d|}rhsJ%By#AUl;rh5dGd!G09nFAf&e5wBt1r;Hc8I?Da_s8d%c|y+b$?1 zH>3^JGWRlza5TFQaNJkaCdhZYMJGoz^IB^I!v^I$3ytOFM#{P+5tqpVaCn>N1Z1X` zg?ud=I@FX*zBVjJPhG6IqYWHe&`V-3AVOeLVU-TgKfbAV^fi-C8~;wcrNKlo{G}t( zZ>Y?2`$Q7!vQULzBsUf5!RN5EW;o(ng%z)@bv|~O^D&wKe@rZsw+gnH4swd(pDXTo z;Dztx?xUiZ1|M2qe8|-Duy7)f&d^!4^)7HIj#{{!8#t)Y^m9`PhSk0`=;s=waaW|AQ5PA~@PK=^k(4NXuQCeQ%4-!c5*GZN;##dq2vvnIs%s z{IUtOKo&)|xFv)m=z2nNf{3U9kVxE3yC#FDYm1#m)kAz<)5wUprJK1+8;|vq=Dbs? z`Zw8xD8l_-kR5VnhaRlvsDoMj4&lCo8`Z*K~*as@Gf z^zZ`hIyh9F?tMjg_zcdv+Y>h2Vvx(C(*P@88&79J!4h zitevQ(LcGU$#96ZKgZrRT|mwzW328z_?oN~tdAG1Gqg2VB1cFH4 zTxf$6pkU2r%yJp@9dYSFquyOSGDffiSHj*dD!I@-L;lm?-Gl))i|GO1;w^h9{eg?%-2dORv z1(^lk!Zyv6smK5L<3bkI#54Txj9KOckWR!8K$;vnOu!BBMa5c|5&B-I1)8%n5CvR4 zIKng1h9lzZ^cosUK>w$`FOO>~*%}NeVsJ-x6tN)!VN+xaBBI?y6Pf@a1j8yI(j@Fl z*qTKIR5n|ZfU*gM009C5VGAH2iyaV^U637u>>z@mg3blC-+Mjt%{TM?=9|A>{lcxf zb?a8$I#qS*oKxoD{nL0;g=uO-zH*_UghG6Dm^8Uk&Yw z$N7Zmhx{fs`r6Nr4Nz5C6A&Sqc(8LEJS90otI)(s)cKKpr2zMU_vlQkkBn?q=R5n5 zc2Ov1<*ko&I)|d2Hss9>d^IK&hBS5bt998|egpz( zWaiY;Hb4N(rT&g>xx8qG$*d^P+6Bjbr#`#>B1?Bf1IL7x@#QN~H?PaJc+cveMTgZg zsAlCWj}Li46$2j{A^&+xz*uhY2Fa1Y#_&K;+)ppSC8QfKE)wuZxEQIl4s>o!-AYRY zO3Hn2YRn>ds(uI-wJU~6F9?*L^CmyTBQ^bvIA<>*2eJp|q10Fm6iS{o*UvqKSg}3? z7z28(-y@w4cKrOPW^!|k{xmUP+3#bcNrW=idRc4a)V0l*>lge!p5>hisrvlF@P$~4 zTtLbCy%aS~^qRxcF2>4@f7kI(@E|xj&#JmFG6YGAg++lWVT0J-1Ss@u3I#~bHQ?k;5QnEclS&CUQ_8bww_^{QDE!38bb8WdlbFK^-oNj_HuJ3y z3kWtFZS@e8(yp>vaF^YlT?+R)^c(;T7|rj!yfM>9ze|tGE7exW5SlM)I97k79q(D( zH9(>FG>l52JjfZSPQ+q|+afF!tYX{xfd%5bxf*A1LPIARZp45KURx4vM(AW?@*uzV z(RospvfF3!#ma&^1iylQ{*XyUX7R~f8BXz0D?W;ufW~*|StWHpi%|);oTLg8pX&ya@8hO}C)H{Z&Y#H0N2T?)K|x(MD=q z70`FCX8?>{O;>x^uQR-2dnYna#fZstQ(+<7+{NxkljE(FnpC?80G(drbqSeA6Jb~B z$rL_Wl)l8oV)G_rEZX;75V!F?tlFR)9R4qB&I)YeeOwDbWX|jsxm6-n#+tuIES^Tu zvk0g#WcKlhtIl>&Wpi)Dzo4D!NlOJ=-mwGBt_{>zTQ}r~TPkex+a7Fpr1K0L6 zGIpofW%cR_42&?^pyT$iDNa5TR(d_YgD~I}s!ogc^!2TAOwz*Xbcwu(g?aw*g3}>H zAuT+~uH0o8;eaAjTQS*hP|G%Xrvx)Z5i4aRH=Oy$MDx0uUXbOiUS7U@R0;{B7IRxCq71?jzp`)?I zBWipr*^^#iX_}d~Hekf3OoN_@(39(aM`4Cd1Vf}jf5S&*+axN+8o)A!rJma`rA8PQZQ7B~p-v6fCYq|g=K_IYx)*pZJ zZM!t$(glpCK-?F1hi^*@WdYNvZC?R^dm>#>sPy4&pAQ#zhXQ#5z~6T-0y`-nf2cq0 zfN4{MX*0Vcd(BwCC3vFj5U;wky^}F{MAYERua(-m=Y!=NoFP{#B2%W@GM7}=(;ScA zEGnH_xq7GnD`lG}w+KR-ttO5Y27 zGU)nx2{^Ey%Sth|w93!3$Alds=%HoV8it*8`jk6Z-}x1^Arh##3UI63{l;3q+V$xq z9E=RqQU#2hrs1VeJKw4aL-w6oZH=ptagwRe#xSsj$&)Psv5?!;DWHbl=4&TqtBi5i zSwd6ZoL$FgC1|rGo(B=m1{l_!M-_3I3R@SuEY; zl-OayGmG~+`C=nd_ElH9Vj&u;gO-j$sTFg9T`K9$vHUUjKo z^OG`JKfVCTN(UX}`7W~(uZ zg3jsYhF$q#*?I9Vk5$s47bUmh1p?MLw<*cY%t|(s3lM0?^+w{)X0Q5^S1V3r68b1G zY%*0?L?iPW7TME(*yh$}mqSyR2Y3`!2k`A~zn(w-aQcJU>9=cDhGi=yZI6gw;!0n1 ze_|6KpE)nM(-p>1*vQu1TSifhQ&2!_I|9=819?oy}Bzgrn!$+W)DAF zrg59R;IK-WZ;i{KGan0lh~&rMA<4f3Rsm8Dy>x&yhV~>lz0h#&tt#hzK6?o>i#&=Y zTNt?i)-=6wk;l-*4zTQf6abLtO-9MuRRs%ymJ7~0gy$Z=uPaOWjcDZf+SumbnS5gK zNTZ$Y^+;I#v%{_w5D4Lo@5l(7&x4Hi?&kjnBGAs3 zxt%_Y*jWqTv;Q?Z|0i!1uv|hx`_n#LR1TDsAD*A}?XR?%YogNe*sj)f1l){V$NWq{7rB)=|0rLKB;o|LZ3Gg$lu4{ zreZmwoTQQK>)GzQ$EO!qC>q|2VutjHu{G|@9(4H%GGMr%Zge#l+<>6?Q!+$y^ zWe)BEm8e2d?U&)DR!nB?-Kie0Np~hA$cj1P4&8&!kwxhBSFqYI*pipmoSvy>Tn82h zahbbh|3}!AN81D>(8qj=aKLn=8QEJqnXm;2JvkBi5WnX0&J0NY?D#LTrnyBTGl5ps zghnIi?p7rgk9a4>%pZV%DkdFnL_x-TgqvF{xW4gSKPzXPJqAPPO^uS`JxaFJMJ(?s zb-;xpX#05i>Dbt%)IrIDqMBkR$)#U$UOnEW8d95P@3!!|JN)qjxf|$CnqF1m-h&dI zO$yepimZY}+d1=tb-OU!&;SLc6;5^8k{vLW#Sb&!nm%A-bQ0xFwb^Mp4k>yPbvkx& z{*4Re`%DKsI{X1l@$cwZGJY?JJ0?AIlKduWobsHux11t}bi?*q96{osayQ6|UB{=Z z?5^kEpdcwfQKd_3eXxLfG7r;zjZhYUNO9reqBRM8v4Pn;)zcoM7GNB6($qlqr3%@` z@p5`$nI*dc@P$NL_zD{3)6a-#LP3o(u*QK&x17kS&~t>N@EExk)ub>0@_;Xq9F^vv zfr{1FQ&)?sv0sukp4c?|v(CR9*7+W5i)ptBuH#3#>>t`aS+MxzJp3_4aB6y+G5H=Z z|Mp(NY$!j(AkHD`bD7&rX&`6QESWmEHSr^EcuBv_K**qDuhX~ zZbTQu%Z)S}4&zq z%0CUCoP`&Vuw`0RCxRXbclR&XYTCZF_JJB)gpySh1Kyw2%Mt1tW@>F5BObA*m@hJ& zx;)5I=&bC*)Zxjpv5uz6?sy6U;p)QNb8=Dl{Gb24>Dd^)UVidt!-cP)BEv1?@-h3H zqrq>e7QBXzeMu1k){5uqawMXC3VCzLCKtuxBif+^HTkcg9%X{o+1SG0Y|AnM^(Fw* zWMbB!bb(aXfvTn{mtTDpc!cGcvufj)K2loP%{D%_OWX(CQ=nr7~0>=eoTN$gYx$kmJ>P=qwb zlGAXA%Yn6P3RmxR=BUy{TTjPWv-Hh;`m%3pb9Fp|KwSm$;4fkmoFHxzAADziilOd> zqA4);r2yCl>y7>(paejb?Q918!pY@_>@J1$)joV0MTOJM&?@gNE$t!lBVJw9ikn%W zAtDE2qphMvv{tEI8R#9x$z5{Kji(t8i=nb@#uhGXa@B&3jj3W$a(VIPxS`8FMLjd} zd1sq_ej2; z5_!oo`;vUDQFI;o71VY8b*)=Vr8+F~>>>q_7d|`P64Y6@`uep`9U%MoTUmSC-c7+qGd7=`z4ki}5rCZ`_O`{l!u=`IsVU|`;SPzsgBV>IU^R>ZOU3;tA1Oyo%abf! zp;@i!*68K}^RL@51Tt^LGLsB>mY+&9ol2_;M z4xF=AJ0#4h);9qp?l-Cyv{yO|1WNx7;J-x@le-2(LxHk;4E?$A+>wLC!dKxK+k4G$ zc~8@E_d0JfHy9c)c^NO}%LMu8Y zB}Yc?{6cjJh+CHrAiKdpy9hfF^| zd-1Dda>i_(|Fb;-9vd@T{#*ZxN4s)P++KUT_WC0`=DqiMv!xTTGik23^S0iK983v5 z(f6p&@$IRz?XT8sYJQ(=@n=coucuto^4JKm4l%)K$c&I6+Xbkw> zgaOHE#zTb?S7HeZJM86I7a0J#_tc*=TOyyV@Ax*1oRiqnR>_{9%*%yVR1e>;(6$CR z1hrmgmh~Zj)iae>o3+X^IX%4TV(m(a zf*R!e`v*Q$DXHgFB*g5V3MsNJ%Lw{AiP2xDBUKrOJI5A{D-f z>?`D+3oYH)0p5RWrV#Q-j6hmoStIJ5NXv(058o1mWlDz;(bFX#w|ME1jCeI$KcoL# zu;j_TFwIu9fku^;;E=nD0bIp+c(r=e7LYDjxo_$Lga2bhH-8-ygaow!@`iGfacuGo z7hCBRKhwd5(?8Laon7kUx=S3G8QW9h|8GzKw;+AK%U5z|nU&8s-Yi!^_CsqR0Oyh#~u=t&PppU zpQQR^CsBVsa5q1^An#Qgj-i=dsn!`&e3eyMrPqUhg|pq850?+q{WfJl7p1ow{1Lc{ zk#oG}Mo*kOVv<&THakT-BPF~Y5(-qE6y1njb0c;vQwO9$1H&{4KU2sPEkLj7{;i94 zE(k*Ga~KY_k2a2jmPC}$t1XCM!v5+5h_?!~E-49|@Nkia8A~;MmyM^P0K!yz=f*8y?*#V&@Y9>B#2xGw#HU$pRF%zaxbc6u_KQs}Ds0 zZR$#Mf#QL9I|v!k+Au~>ti@>2z`>+`XsH zmjzw!UWf7h+BOh!-SGVVf>c6G+MxOZKo&y zxF}NfFyU|Ia^XUT`b!G*u>3ulHU8>7iF%BpaofvBwFGA2$z+Ffe(Zr>g`8;Puvn!3 zK>LJx5wVw*Q_p9`mxom1HyD9ex+&6c(ZRWm&s;xKDg)c_b`wxR>nI<{_%rNi+LzBi zNt{PIv0NZQ0u{r9rYsO`Og{H1D*4?Tg!hmjia$q2w7I!3(pdq6$?TCiR!UC5nX^}2 z<%-a1_$oLbc=dnM_~NSj`FdZ|s1Wq_!UZQ1N#8m)M8NURYd_Z2yB)NHHqs{KjB3+B zA4A}yB+vEhh*Mm z)LA%p=Hp%_RU^qW-Ts~AQlH#aAL=pJUfP}d8nt7}QbOaa@g*gL@%rzsd4>W*wpV)l z`aLLQ=qKc5{9f^-Hi~l(tU>AA%DP^dgqOmS4AeY}m@`D=1x%d-2jvViM9b@(FMdyc3IS_w8Gc8tJh4$d!3ukr45@ z*=@RL^S0X4pz(@TvoF7hNe)j{8rGP7iTVoqGmUWTb8`srJoFV$O*pIiE9jCbZ;-iu zK01o4_?T7*%77>z|M=DOC78pszfZ^=vHJcv2@U`3&gqicCihou6DXvmfNO0!VV9kz z4`qsko<>-Sy!UmPT?f=6`>+0n8}=N@iS8Jdij1gnV(ai}Y}WVgX>Gi$9IH-ixQzPz z$MPJcJIz(7tLA{aix=Ma6WapN&&l8EC3m_-I|u04K{utsub@|aHvu=t01#j`+n=cm z<*E4wg&p{Sa)sXe`}lsj_3tEA1>OD%mxK zvu88WUdQ*&`?VkV{{Sid62|}l literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png b/web-player/design_handoff_musify_player/screenshots/08-sin-resultados.png new file mode 100644 index 0000000000000000000000000000000000000000..98ebf7f40f98f4b6128c31e948b795d472e7af53 GIT binary patch literal 28546 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 literal 0 HcmV?d00001 diff --git a/web-player/design_handoff_musify_player/support.js b/web-player/design_handoff_musify_player/support.js new file mode 100644 index 0000000..712aa3b --- /dev/null +++ b/web-player/design_handoff_musify_player/support.js @@ -0,0 +1,1953 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +'use strict'; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => + key in obj + ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) + : (obj[key] = value); + var __publicField = (obj, key, value) => + __defNormalProp(obj, typeof key !== 'symbol' ? key + '' : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error('dc-runtime: window.React is not available yet'); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error('dc-runtime: window.ReactDOM is not available yet'); + return RD; + } + var h = (...args) => getReact().createElement(...args); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector('x-dc'); + if (!dc) return null; + const scriptEl = doc.querySelector('script[data-dc-script]'); + const { props, preview } = parseDataProps(scriptEl?.getAttribute('data-props') ?? null); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || '' : '', + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(''); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, 'text/html'); + const scriptEl = doc.querySelector('script[data-dc-script]'); + const { props, preview } = parseDataProps(scriptEl?.getAttribute('data-props') ?? null); + return { + template, + js: scriptEl ? scriptEl.textContent || '' : '', + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === 'object' ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== '$') rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ''; + try { + p = decodeURIComponent(p); + } catch {} + const base = p.split('/').pop() || 'Root'; + return base.replace(/\.dc\.html$/, '').replace(/\.html?$/, '') || 'Root'; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = 'html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}'; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ''; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || '/').pathname; + } catch {} + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + if (!window.__resources) { + fetch(location.href) + .then((res) => (res.ok ? res.text() : '')) + .then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }) + .catch(() => {}); + } + const dc = doc.querySelector('x-dc'); + const hostEl = doc.createElement('div'); + hostEl.id = 'dc-root'; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement('style'); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...(entry.propOverrides || {}) }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === '(' && expr[expr.length - 1] === ')' && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case '===': + return lv === rv; + case '!==': + return lv !== rv; + case '==': + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === '!') return !resolve(vals, expr.slice(1)); + if (expr === 'true') return true; + if (expr === 'false') return false; + if (expr === 'null') return null; + if (expr === 'undefined') return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if ( + expr.length >= 2 && + (expr[0] === '"' || expr[0] === "'") && + expr[expr.length - 1] === expr[0] + ) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === '(') depth++; + else if (expr[i] === ')') { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === '[' || c === '(') depth++; + else if (c === ']' || c === ')') depth--; + else if (depth === 0 && (c === '=' || c === '!') && expr[i + 1] === '=') { + if (i > 0 && (expr[i - 1] === '=' || expr[i - 1] === '!')) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === '=' ? c + '==' : c + '='; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === '.') { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === '[') { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === '[') depth++; + else if (expr[j] === ']') { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = 'sc-camel-'; + var INLINE_TEXT_TAGS = new Set( + 'a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr'.split( + ' ' + ) + ); + var RAW_WRAP = { + select: 'sc-raw-select', + table: 'sc-raw-table', + tbody: 'sc-raw-tbody', + thead: 'sc-raw-thead', + tfoot: 'sc-raw-tfoot', + tr: 'sc-raw-tr', + td: 'sc-raw-td', + th: 'sc-raw-th', + caption: 'sc-raw-caption' + }; + var RAW_UNWRAP = Object.fromEntries(Object.entries(RAW_WRAP).map(([k, v]) => [v, k])); + var EVENT_MAP = { + onclick: 'onClick', + onchange: 'onChange', + oninput: 'onInput', + onsubmit: 'onSubmit', + onkeydown: 'onKeyDown', + onkeyup: 'onKeyUp', + onkeypress: 'onKeyPress', + onmousedown: 'onMouseDown', + onmouseup: 'onMouseUp', + onmouseenter: 'onMouseEnter', + onmouseleave: 'onMouseLeave', + onfocus: 'onFocus', + onblur: 'onBlur', + ondoubleclick: 'onDoubleClick', + oncontextmenu: 'onContextMenu', + onmousemove: 'onMouseMove', + onmouseover: 'onMouseOver', + onmouseout: 'onMouseOut', + onpointerdown: 'onPointerDown', + onpointerup: 'onPointerUp', + onpointermove: 'onPointerMove', + onpointerenter: 'onPointerEnter', + onpointerleave: 'onPointerLeave', + onpointercancel: 'onPointerCancel', + onpointerover: 'onPointerOver', + onpointerout: 'onPointerOut', + ongotpointercapture: 'onGotPointerCapture', + onlostpointercapture: 'onLostPointerCapture', + ontouchstart: 'onTouchStart', + ontouchend: 'onTouchEnd', + ontouchmove: 'onTouchMove', + ontouchcancel: 'onTouchCancel', + ondragstart: 'onDragStart', + ondragend: 'onDragEnd', + ondragenter: 'onDragEnter', + ondragleave: 'onDragLeave', + ondragover: 'onDragOver', + onanimationstart: 'onAnimationStart', + onanimationend: 'onAnimationEnd', + onanimationiteration: 'onAnimationIteration', + ontransitionend: 'onTransitionEnd' + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp('<(x-import|dc-import)(' + ATTRS + ')/>', 'gi'); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCamelAttrs(html) { + return html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => + sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()) + eq + ); + } + function encodeCase(html) { + html = html.replace(IMPORT_SELF_CLOSE_RE, (_, t, a) => '<' + t + a + '>'); + html = html.replace(/)/gi, '/gi, ''); + html = encodeCamelAttrs(html); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace(new RegExp('(])', 'gi'), '$1' + alias); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(';')) { + const i = decl.indexOf(':'); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith('--') ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes('{{')) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => (i & 1 ? (resolve(vals, s) ?? '') : s)).join(''); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === 'sc-name' || name === 'data-dc-tpl') continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === 'hint-size') { + hintSize = value; + continue; + } + if (key.startsWith('style-')) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== 'dom') { + if ( + key.includes('-') && + !(kind === 'x-import' && (key.startsWith('aria-') || key.startsWith('data-'))) + ) + key = kebabToCamel(key); + } else { + if (key === 'class') key = 'className'; + else if (key === 'for') key = 'htmlFor'; + else if (key.startsWith('on')) + key = EVENT_MAP[key] || 'on' + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + 'position', + 'left', + 'right', + 'top', + 'bottom', + 'inset', + 'width', + 'height', + 'z-index', + 'transform' + ]); + function hostPositionStyle(style) { + const all = + typeof style === 'string' + ? cssToObj(style) + : style != null && typeof style === 'object' + ? style + : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement('template'); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute('data-dc-tpl', String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = (vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i)); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + var SLIDE_ID_VALUE_RE = /^[0-9a-f]{8}$/; + var DECK_CONTROL_FLOW_RE = /^(sc-if|sc-for|sc-else|dc-import|x-import)$/; + var DECK_AUX_RE = /^(template|script|style|sc-helmet|helmet)$/; + function isDeckMountTag(el) { + if (el.localName === 'deck-stage') return true; + return ( + el.localName === 'x-import' && + (el.getAttribute('component-from-global-scope') || '') === 'deck-stage' + ); + } + function walkDeckChildren(el, host) { + const pairs = [...el.childNodes] + .map((c) => ({ c, b: walk(c, host) })) + .filter((p) => p.b !== null); + const kids = pairs.map((p) => p.b); + const seen = /* @__PURE__ */ new Set(); + const wsSeen = /* @__PURE__ */ new Map(); + const keys = []; + const nextSlideId = new Array(pairs.length); + { + let upcoming = null; + for (let j = pairs.length - 1; j >= 0; j--) { + const n = pairs[j].c; + if (n.nodeType === Node.ELEMENT_NODE) { + const t = n.localName; + upcoming = + !DECK_AUX_RE.test(t) && !DECK_CONTROL_FLOW_RE.test(t) + ? n.getAttribute('data-om-slide-id') + : null; + } + nextSlideId[j] = upcoming; + } + } + for (let j = 0; j < pairs.length; j++) { + const { c } = pairs[j]; + if (c.nodeType === Node.TEXT_NODE) { + if ((c.nodeValue ?? '').trim() === '') { + const base = nextSlideId[j] ? 'omid-ws:' + nextSlideId[j] : 'omid-ws:aux'; + const n = wsSeen.get(base) ?? 0; + wsSeen.set(base, n + 1); + keys.push(n === 0 ? base : base + ':' + n); + continue; + } + return { kids, keys: null }; + } + if (c.nodeType !== Node.ELEMENT_NODE) { + keys.push(j); + continue; + } + const child = c; + const tag = child.localName; + if (DECK_AUX_RE.test(tag)) { + keys.push(j); + continue; + } + if (DECK_CONTROL_FLOW_RE.test(tag)) return { kids, keys: null }; + const v = child.getAttribute('data-om-slide-id'); + if (!v || !SLIDE_ID_VALUE_RE.test(v) || seen.has(v)) { + return { kids, keys: null }; + } + seen.add(v); + keys.push('omid:' + v); + } + return { kids, keys }; + } + function renderDeckKids(kids, kidKeys, vals, ctx) { + return kids.map((b, j) => { + const k = kidKeys ? kidKeys[j] : j; + const out = b(vals, ctx, k); + return kidKeys != null && typeof out === 'string' + ? h(getReact().Fragment, { key: k }, out) + : out; + }); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === 'sc-for') return walkFor(el, host); + if (tag === 'sc-if') return walkIf(el, host); + if (tag === 'x-import') return walkXImport(el, host); + if (tag === 'sc-helmet') return host.helmet(el); + if (tag === 'dc-import') return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || '?') + '\0' + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn('[dc-runtime] ' + (ctx?.__name || 'template') + ': ' + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ''; + if (!txt.includes('{{')) { + if (!txt.trim() && !txt.includes(' ')) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => + h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute('data-dc-editor-on')) { + return h( + 'span', + { key: i, className: 'sc-interp sc-unresolved' }, + '{{ ' + p.trim() + ' }}' + ); + } + warnUnresolved(ctx, '{{ ' + p.trim() + ' }} never resolved \u2014 rendered as empty'); + return null; + } + return h('span', { key: i, className: 'sc-interp sc-missing' }, p.trim()); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === 'boolean') return null; + return h('span', { key: i, className: 'sc-interp' }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute('list') || ''); + const asName = el.getAttribute('as') || 'item'; + const hintN = parseInt(el.getAttribute('hint-placeholder-count') || '0', 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute('list') || ''; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ')' + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute('value') || ''); + const hintRaw = el.getAttribute('hint-placeholder-val'); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v + ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) + : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute('name') || el.getAttribute('component') || ''; + el.removeAttribute('name'); + el.removeAttribute('component'); + const tplId = el.getAttribute('data-dc-tpl'); + const styleRaw = el.getAttribute('style'); + el.removeAttribute('style'); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, 'dc-import', host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === 'dcProps') { + if (v && typeof v === 'object') Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr(el.getAttribute('component-from-global-scope') || ''); + const exportNameGet = compileAttr( + el.getAttribute('component') || el.getAttribute('name') || '' + ); + const fromRaw = + el.getAttribute('from') || + (el.getAttribute('component-from-global-scope') + ? '' + : el.getAttribute('src') || el.getAttribute('import') || ''); + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ''; + const kindOf = (u) => (/\.(jsx|tsx)(\?|#|$)/i.test(u) ? 'jsx' : 'js'); + const tplId = el.getAttribute('data-dc-tpl'); + const styleRaw = el.getAttribute('style'); + el.removeAttribute('style'); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, 'x-import', host); + const hasContent = el.children.length > 0 || !!(el.textContent || '').trim(); + const deckKeyed = hasContent && isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : hasContent ? walkChildren(el, host) : []; + const kidKeys = deckKeyed?.keys ?? null; + const urlBindable = fromRaw.includes('{{'); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? '' : String(v); + return s.includes('{{') ? '' : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = + !name || urlBindable + ? null + : globalName + ? host.resolveExternalGlobal(url, globalName) + : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap + ? { + key, + className: 'sc-host-x', + 'data-dc-tpl': tplId, + style: hostStyle || { display: 'contents' } + } + : null; + if (!C) { + const error = urlBindable + ? 'x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL' + : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h('div', wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === 'component' || k === 'componentFromGlobalScope' || k === 'from') { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === 'dcProps') { + if (v && typeof v === 'object') Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h('div', wrapper, ph) : ph; + } + if (kids.length) { + props.children = renderDeckKids(kids, kidKeys, vals, ctx); + } + return wrapper ? h('div', wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll('*')) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = ((h2 << 5) + h2 + s.charCodeAt(i)) | 0; + return s.length + '.' + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + 'script style textarea option title select canvas iframe video audio'.split(' ') + ); + var NOT_INLINE_SELECTOR = ':not(' + [...INLINE_TEXT_TAGS].join(',') + ')'; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute('data-dc-tpl'); + const inlineOnly = + el.childNodes.length > 0 && + !NEVER_CONTENT_KEYED.has(realTag) && + el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? '|' + contentKey(el) : ''; + const { propGetters, pseudoClasses } = collectProps(el, 'dom', host); + const deckKeyed = isDeckMountTag(el) ? walkDeckChildren(el, host) : null; + const kids = deckKeyed ? deckKeyed.kids : walkChildren(el, host); + const kidKeys = deckKeyed?.keys ?? null; + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + 'data-dc-tpl': tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === 'style' && typeof v === 'string') v = cssToObj(v); + if ((k === 'value' || k === 'checked') && v === void 0) { + v = k === 'checked' ? false : ''; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(' '); + } + return h(realTag, props, ...renderDeckKids(kids, kidKeys, vals, ctx)); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, 'props'); + __publicField(this, 'state', {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, '__host'); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() {} + componentDidUpdate(_prevProps) {} + componentWillUnmount() {} + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + 'DCLogic', + 'StreamableLogic', + 'React', + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== 'children'); + const bk = Object.keys(b).filter((k) => k !== 'children'); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ name, hintSize, streaming, error }) { + const [w, hgt] = (hintSize || '100%,60px').split(','); + return h( + 'div', + { + className: 'sc-placeholder' + (streaming ? ' sc-streaming' : ''), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error + ? h('div', { className: 'sc-placeholder-error' }, (name ? name + ': ' : '') + error) + : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(','); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, '__name'); + __publicField(this, '__sub'); + __publicField(this, '__needsDidMount', false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, '__streamingNow', false); + __publicField(this, '__htmlStreamingNow', false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, '__failedLogic', null); + __publicField(this, '__failedUserProps', null); + __publicField(this, '__failedVer', -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, '__ctorError', null); + __publicField(this, 'logic'); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + '[dc-runtime] render error in <' + this.__name + '>:', + e, + info?.componentStack || '' + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = + this.__name + ': ' + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic(this.__userProps()); + } + this.logic.__host = this; + if (prevState) this.logic.state = { ...(this.logic.state || {}), ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === 'function' ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if ( + Next === Cur || + (!Next && Cur === StreamableLogic) || + (Next === this.__failedLogic && + r.ver === this.__failedVer && + shallowEqual(this.__userProps(), this.__failedUserProps)) + ) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = + 'sc-host' + + (r.htmlStreaming ? ' sc-streaming-html' : '') + + (r.jsStreaming ? ' sc-streaming-js' : ''); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = + this.props.__hostStyle || hintStyle + ? { ...(hintStyle || {}), ...(this.props.__hostStyle || {}) } + : void 0; + const hostBase = { + className: cls, + style: hostStyle, + 'data-sc-name': this.__name, + 'data-dc-tpl': this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [...chain.slice(chain.indexOf(this.__name)), this.__name].join(' \u2192 '); + return h( + 'div', + { ...hostBase, className: cls + ' sc-has-error' }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: 'circular import: ' + cycle + }) + ); + } + if (this.state.__err) { + return h( + 'div', + { ...hostBase, className: cls + ' sc-has-error' }, + h( + 'div', + { className: 'sc-logic-error', 'data-omelette-chrome': '' }, + this.__name + ': ' + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + 'div', + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...(this.logic.renderVals() || {}) }; + } catch (e) { + console.error(e); + renderErr = + this.__name + + '.renderVals(): ' + + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + 'div', + { ...hostBase, className: cls + (renderErr ? ' sc-has-error' : '') }, + renderErr && + h('div', { className: 'sc-logic-error', 'data-omelette-chrome': '' }, renderErr), + h(AncestorContext.Provider, { value: [...chain, this.__name] }, r.tpl(vals, this)) + ); + } + } + __publicField(StreamableComponent, 'contextType', AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/bundled.ts + function bundledBlob(url) { + const blobs = window.__resourceBlobs; + const b = blobs ? blobs[url.split('#')[0]] : void 0; + return b instanceof Blob ? b : null; + } + + // src/cdn.ts + var REACT_URL = 'https://unpkg.com/react@18.3.1/umd/react.production.min.js'; + var REACT_SRI = 'sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z'; + var REACT_DOM_URL = 'https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js'; + var REACT_DOM_SRI = 'sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1'; + var BABEL_URL = 'https://unpkg.com/@babel/standalone@7.29.0/babel.min.js'; + var BABEL_SRI = 'sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y'; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === 'string' && v ? { src: v } : { src: url, integrity: sri }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes('.') && n.includes('-'); + function isRenderableType(g) { + if (typeof g === 'function') return !isElementClass(g); + return typeof g === 'object' && g !== null && typeof g.$$typeof === 'symbol'; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split('.')) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement('script'); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + s.crossOrigin = 'anonymous'; + } + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info('[dc-runtime] x-import: loading', url, '(' + kind + ')'); + const ready = Promise.all([ + kind === 'jsx' ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready + .then(() => { + const pre = bundledBlob(url); + if (pre) return pre.text(); + return fetch(url).then((r) => { + if (!r.ok) throw new Error('HTTP ' + r.status); + return r.text(); + }); + }) + .then((src) => { + const code = + kind === 'jsx' + ? window.Babel.transform(src, { + filename: url, + presets: ['react', 'typescript'] + }).code + : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function('React', 'module', 'exports', 'require', code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === 'function') { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + '[dc-runtime] x-import: loaded', + url, + '\u2014 exports:', + Object.keys(module.exports), + 'window globals:', + Object.keys(globals) + ); + onResolved(); + }) + .catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: 'failed to load: ' + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error('[dc-runtime] x-import: FAILED to load', url, '(' + kind + ')', e); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = + (mod && mod[name]) || + (globals && globals[name]) || + (typeof window !== 'undefined' && window[name]) || + (mod && mod.default); + if (typeof C === 'function') return C; + const key = url + '\0' + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(', ') + ')' + ); + console.error( + '[dc-runtime] x-import: module', + url, + 'loaded but has no component named', + JSON.stringify(name), + '\u2014 available exports:', + Object.keys(mod), + 'window globals:', + Object.keys(globals), + '. The module must `module.exports = {' + name + '}` or set `window.' + name + '`.' + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE + ? customElements.get(name) + : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + '[dc-runtime] x-import: global', + JSON.stringify(name), + 'never appeared on window after ' + GLOBAL_POLL_TIMEOUT_MS + 'ms' + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes('.')) return null; + const key = url + '\0global\0' + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + '[dc-runtime] x-import:', + url, + 'loaded but no custom element', + JSON.stringify(name), + 'is registered and window.' + + name + + ' is not a function \u2014 rendering <' + + name + + '> as an unknown element.' + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + '\0' + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return ( + typeof g === 'function' && + typeof HTMLElement !== 'undefined' && + g.prototype instanceof HTMLElement + ); + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = + // layout + '.fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}'; + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = + /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = '#f0eee6'; + var CANVAS_BG_DARK = '#2e2c26'; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = 'light'; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = + ds === 'dark' || ds === 'light' + ? ds + : new URLSearchParams(doc.defaultView?.location.search ?? '').get('theme') === 'dark' + ? 'dark' + : 'light'; + } catch {} + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === 'dark' ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: '__dc_design_mode', mode }, '*'); + } catch {} + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === 'canvas') { + doc.documentElement.setAttribute('data-dc-canvas', ''); + canvasStyleEl = doc.createElement('style'); + canvasStyleEl.setAttribute('data-dc-canvas', ''); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute('data-dc-canvas'); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener('message', (e) => { + const type = e.data && e.data.type; + if (type === '__dc_theme') { + const t = e.data.theme; + if (t === 'light' || t === 'dark') { + appTheme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== '__dc_probe') return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute('data-dc-atomics') && !mounted.has('__dc-atomics')) { + mounted.add('__dc-atomics'); + const el = doc.createElement('style'); + el.id = '__dc-atomics'; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = (ctx && ctx.__name) || ''; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === 'SCRIPT') { + if (mayBePartial) continue; + const key = 'SCRIPT|' + (child.getAttribute('src') || child.textContent || ''); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement('script'); + for (const { name: an, value } of [...child.attributes]) el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === 'LINK' || tag === 'META') { + if (mayBePartial) continue; + const key = + tag + + '|' + + (child.getAttribute('href') || child.getAttribute('src') || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + if (tag === 'LINK') { + const rel = (child.getAttribute('rel') || '').toLowerCase().split(/\s+/); + const href = (child.getAttribute('href') || '').trim(); + const res = window.__resources; + const pre = + res && rel.includes('stylesheet') && !rel.includes('alternate') + ? res[href] + : void 0; + const blob = typeof pre === 'string' && pre ? bundledBlob(pre) : null; + if (blob) { + const el = doc.createElement('style'); + if (child.hasAttribute('disabled')) { + el.setAttribute('media', 'not all'); + } else if (child.getAttribute('media')) { + el.setAttribute('media', child.getAttribute('media')); + } + if (child.getAttribute('title')) + el.setAttribute('title', child.getAttribute('title')); + void blob.text().then((css) => { + el.textContent = css; + }); + doc.head.appendChild(el); + continue; + } + } + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + '|' + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function scanUnquotedUrl(css, i) { + if ( + (css[i] !== 'u' && css[i] !== 'U') || + css.slice(i, i + 4).toLowerCase() !== 'url(' || + /[a-z0-9_-]/i.test(css[i - 1] ?? '') + ) { + return -1; + } + let j = i + 4; + while (j < css.length && /\s/.test(css[j])) j++; + if (css[j] === '"' || css[j] === "'") return -1; + while (j < css.length && css[j] !== ')') { + if (css[j] === '\\') j++; + j++; + } + return j < css.length ? j + 1 : css.length; + } + function stripComments(css) { + let out = ''; + let quote = ''; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === '\\') { + out += c + (css[i + 1] ?? ''); + i++; + continue; + } + if (c === quote) quote = ''; + out += c; + } else if (c === "'" || c === '"') { + quote = c; + out += c; + } else if (c === '/' && css[i + 1] === '*') { + const end = css.indexOf('*/', i + 2); + i = end === -1 ? css.length : end + 1; + out += ' '; + } else { + const end = scanUnquotedUrl(css, i); + if (end === -1) out += c; + else { + out += css.slice(i, end); + i = end - 1; + } + } + } + return out; + } + function importantify(css) { + css = stripComments(css); + const decls = []; + let start = 0; + let depth = 0; + let quote = ''; + for (let i = 0; i < css.length; i++) { + const c = css[i]; + if (quote) { + if (c === '\\') i++; + else if (c === quote) quote = ''; + } else if (c === "'" || c === '"') quote = c; + else if (c === '(') depth++; + else if (c === ')') depth = Math.max(0, depth - 1); + else if (c === ';' && depth === 0) { + decls.push(css.slice(start, i)); + start = i + 1; + } else { + const end = scanUnquotedUrl(css, i); + if (end !== -1) i = end - 1; + } + } + decls.push(css.slice(start)); + return decls + .map((d) => d.trim()) + .filter(Boolean) + .map((d) => (/!\s*important$/i.test(d) ? d : d + ' !important')) + .join(';'); + } + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + '|' + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement('style'); + doc.head.appendChild(el); + } + const cls = 'scp' + (n++).toString(36); + const isPseudoElement = pseudo === 'before' || pseudo === 'after'; + const sel = isPseudoElement ? '.' + cls + '::' + pseudo : '.' + cls + ':' + pseudo; + el.sheet.insertRule( + sel + '{' + (isPseudoElement ? css : importantify(css)) + '}', + el.sheet.cssRules.length + ); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return ( + entries[name] || + (entries[name] = { + html: '', + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }) + ); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = '.'; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager(doc, (name) => registry.get(name).htmlStreaming); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + '/' + encodeURIComponent(name) + '.dc.html'; + const res = window.__resources; + const pre = res ? res[url] : void 0; + const target = typeof pre === 'string' && pre ? pre : url; + const blob = bundledBlob(target); + (blob + ? blob.text() + : fetch(target).then((res2) => { + if (!res2.ok) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '" failed:', + url, + 'returned', + res2.status, + '\u2014 the reference renders as an empty placeholder.' + ); + return ''; + } + return res2.text(); + }) + ) + .then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '":', + url, + 'has no block \u2014 not a Design Component.' + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }) + .catch((e) => + console.error('[dc-runtime] sibling fetch for "' + name + '" threw:', url, e) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error('[dc-runtime] template compile FAILED for', name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = (r.jsSeq = (r.jsSeq || 0) + 1); + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== 'function') { + r.logicError = + name + + '.dc.html: diff --git a/web-player/src/lib/components/MobileNav.svelte b/web-player/src/lib/components/MobileNav.svelte deleted file mode 100644 index ca717c1..0000000 --- a/web-player/src/lib/components/MobileNav.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/web-player/src/lib/components/SearchBox.svelte b/web-player/src/lib/components/SearchBox.svelte new file mode 100644 index 0000000..551566e --- /dev/null +++ b/web-player/src/lib/components/SearchBox.svelte @@ -0,0 +1,86 @@ + + +
+
+ +
+
diff --git a/web-player/src/lib/components/Sidebar.svelte b/web-player/src/lib/components/Sidebar.svelte index ca7ee29..15cb9c4 100644 --- a/web-player/src/lib/components/Sidebar.svelte +++ b/web-player/src/lib/components/Sidebar.svelte @@ -1,117 +1,129 @@ -{#snippet navLink(link: { href: string; label: string; icon: typeof Home })} - {@const active = isActive(link.href)} - - - {link.label} - -{/snippet} - -{#snippet groupLabel(label: string)} -

- {label} -

-{/snippet} - diff --git a/web-player/src/lib/components/TabsBar.svelte b/web-player/src/lib/components/TabsBar.svelte new file mode 100644 index 0000000..9261466 --- /dev/null +++ b/web-player/src/lib/components/TabsBar.svelte @@ -0,0 +1,50 @@ + + + diff --git a/web-player/src/lib/components/TopBar.svelte b/web-player/src/lib/components/TopBar.svelte index 3778001..ed66c21 100644 --- a/web-player/src/lib/components/TopBar.svelte +++ b/web-player/src/lib/components/TopBar.svelte @@ -1,220 +1,104 @@ -
- Musify +
+ + + Musify + -
- {#if user} - - - {#if menuOpen} -
+ {#if user} +
+ {#if user.picture} + + {:else} + + {user.name.charAt(0)} + + {/if} + + + {#if menuOpen} + + (menuOpen = false)} + /> + (menuOpen = false)} + /> + (menuOpen = false)} + /> + {#if accountUrl} + (menuOpen = false)} + /> + {/if} + themeMode.toggle()} + /> +
+ + +
+ {/if} + {:else} + + + {/if} - {:else} - - - - {/if} +
+ +