From 7ae6c0a7a2caba4322968404dad8dbf70696adab Mon Sep 17 00:00:00 2001 From: XShine555 Date: Thu, 10 Sep 2026 19:19:31 +0200 Subject: [PATCH] Split AlbumApplicationResponse into a Local/External hierarchy AlbumApplicationResponse used to carry YouTubeAlbumId/ThumbnailUrl as two always-present-but-usually-null fields for the sake of a single flat shape. Since one of these fields IS genuinely exclusive to external albums, an inheritance split reads better than the flattened shape: AlbumApplicationResponse now holds only the fields shared by every album, and ExternalAlbumApplicationResponse adds ExternalId (renamed from YouTubeAlbumId, matching the source-generalized naming used everywhere else) and ThumbnailUrl. [JsonPolymorphic]/[JsonDerivedType] are required here, not optional: without them System.Text.Json serializes strictly by the declared CLR type at each call site (both the bare return type and nested inside AlbumSearchItemResponse.Album), so an ExternalAlbumApplicationResponse instance would silently lose its extra fields on the wire. This also gives every response a "source" discriminator field for free, consistent with AlbumSearchItemResponse.Source, and the generated OpenAPI schema now models it as a proper oneOf/discriminator instead of nullable fields. --- .../Responses/AlbumApplicationResponse.cs | 36 +++++++------- .../Responses/AlbumSearchItemResponse.cs | 2 +- .../ExternalAlbumApplicationResponse.cs | 47 +++++++++++++++++++ web-player/src/routes/+page.svelte | 10 ++-- 4 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 backend/Core/Musify.Application/Albums/Responses/ExternalAlbumApplicationResponse.cs diff --git a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs index c1e6948..0323ded 100644 --- a/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs +++ b/backend/Core/Musify.Application/Albums/Responses/AlbumApplicationResponse.cs @@ -1,7 +1,17 @@ +using System.Text.Json.Serialization; using Musify.Domain.Entities; namespace Musify.Application.Albums.Responses { + // [JsonPolymorphic]/[JsonDerivedType] are required here, not optional: without them + // System.Text.Json serializes strictly by the declared CLR type at each call site + // (AlbumApplicationResponse, both as a bare return type and nested inside + // AlbumSearchItemResponse.Album), so an ExternalAlbumApplicationResponse instance would + // silently lose ExternalId/ThumbnailUrl on the wire. This also gives every response a + // "source" discriminator field for free, matching AlbumSearchItemResponse.Source. + [JsonPolymorphic(TypeDiscriminatorPropertyName = "source")] + [JsonDerivedType(typeof(AlbumApplicationResponse), typeDiscriminator: "Local")] + [JsonDerivedType(typeof(ExternalAlbumApplicationResponse), typeDiscriminator: "YouTube")] public record AlbumApplicationResponse( Guid Id, string Title, @@ -14,9 +24,7 @@ namespace Musify.Application.Albums.Responses string? LargeImageKeyName, DateTime CreatedAt, DateTime UpdatedAt, - IReadOnlyList CoverTrackIds, - string? YouTubeAlbumId = null, - string? ThumbnailUrl = null) + IReadOnlyList CoverTrackIds) { public const int CoverTrackCount = 4; @@ -45,22 +53,12 @@ namespace Musify.Application.Albums.Responses int trackCount, IReadOnlyList? coverTrackIds = null) { - var externalAlbum = album as ExternalAlbum; - return new AlbumApplicationResponse( - album.Id, - album.Title, - album.Description, - album.ReleaseYear, - album is UserAlbum userAlbum ? userAlbum.OwnerUserId : 0, - trackCount, - album.Pictures?.SmallName, - album.Pictures?.MediumName, - album.Pictures?.LargeName, - album.CreatedAt, - album.UpdatedAt, - coverTrackIds ?? [], - externalAlbum?.ExternalId, - externalAlbum?.ThumbnailUrl); + return album switch + { + ExternalAlbum externalAlbum => ExternalAlbumApplicationResponse.FromEntity(externalAlbum, trackCount, coverTrackIds), + UserAlbum userAlbum => FromEntity(userAlbum, trackCount, coverTrackIds), + _ => throw new NotSupportedException($"Unsupported album entity type '{album.GetType().Name}'.") + }; } } } diff --git a/backend/Core/Musify.Application/Albums/Responses/AlbumSearchItemResponse.cs b/backend/Core/Musify.Application/Albums/Responses/AlbumSearchItemResponse.cs index 5789edd..40acd5a 100644 --- a/backend/Core/Musify.Application/Albums/Responses/AlbumSearchItemResponse.cs +++ b/backend/Core/Musify.Application/Albums/Responses/AlbumSearchItemResponse.cs @@ -14,7 +14,7 @@ namespace Musify.Application.Albums.Responses YouTubeAlbumResult? YouTubeAlbum) { public static AlbumSearchItemResponse FromAlbum(AlbumApplicationResponse album) => - new(album.YouTubeAlbumId is not null ? TrackSource.YouTube : TrackSource.Local, album, null); + new(album is ExternalAlbumApplicationResponse ? TrackSource.YouTube : TrackSource.Local, album, null); public static AlbumSearchItemResponse FromYouTubeAlbum(YouTubeAlbumResult album) => new(TrackSource.YouTube, null, album); diff --git a/backend/Core/Musify.Application/Albums/Responses/ExternalAlbumApplicationResponse.cs b/backend/Core/Musify.Application/Albums/Responses/ExternalAlbumApplicationResponse.cs new file mode 100644 index 0000000..a9f12a3 --- /dev/null +++ b/backend/Core/Musify.Application/Albums/Responses/ExternalAlbumApplicationResponse.cs @@ -0,0 +1,47 @@ +using Musify.Domain.Entities; + +namespace Musify.Application.Albums.Responses +{ + public record ExternalAlbumApplicationResponse( + Guid Id, + string Title, + string? Description, + int? ReleaseYear, + long OwnerUserId, + int TrackCount, + string? SmallImageKeyName, + string? MediumImageKeyName, + string? LargeImageKeyName, + DateTime CreatedAt, + DateTime UpdatedAt, + IReadOnlyList CoverTrackIds, + string ExternalId, + string? ThumbnailUrl) + : AlbumApplicationResponse( + Id, Title, Description, ReleaseYear, OwnerUserId, TrackCount, + SmallImageKeyName, MediumImageKeyName, LargeImageKeyName, + CreatedAt, UpdatedAt, CoverTrackIds) + { + public static ExternalAlbumApplicationResponse FromEntity( + ExternalAlbum album, + int trackCount, + IReadOnlyList? coverTrackIds = null) + { + return new ExternalAlbumApplicationResponse( + album.Id, + album.Title, + album.Description, + album.ReleaseYear, + 0, + trackCount, + album.Pictures?.SmallName, + album.Pictures?.MediumName, + album.Pictures?.LargeName, + album.CreatedAt, + album.UpdatedAt, + coverTrackIds ?? [], + album.ExternalId, + album.ThumbnailUrl); + } + } +} diff --git a/web-player/src/routes/+page.svelte b/web-player/src/routes/+page.svelte index 8b3cbc1..b59f9e9 100644 --- a/web-player/src/routes/+page.svelte +++ b/web-player/src/routes/+page.svelte @@ -155,7 +155,9 @@ >

{data.greeting}

-

+

Tu música. Sin límites.

@@ -214,15 +216,15 @@ {#each albums as album, i (album.id)} openAlbumMenu(e, album.id)} >
- {#if album.thumbnailUrl} + {#if album.source === 'YouTube' && album.thumbnailUrl}