Design Prot Merge #1
@@ -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<Guid> CoverTrackIds,
|
||||
string? YouTubeAlbumId = null,
|
||||
string? ThumbnailUrl = null)
|
||||
IReadOnlyList<Guid> CoverTrackIds)
|
||||
{
|
||||
public const int CoverTrackCount = 4;
|
||||
|
||||
@@ -45,22 +53,12 @@ namespace Musify.Application.Albums.Responses
|
||||
int trackCount,
|
||||
IReadOnlyList<Guid>? 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}'.")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Guid> 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<Guid>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +155,9 @@
|
||||
></div>
|
||||
<div class="animate-enter relative flex flex-col gap-2 sm:gap-2.5">
|
||||
<p class="text-sm tracking-[0.14em] text-fg-2 uppercase sm:text-base">{data.greeting}</p>
|
||||
<h1 class="font-display text-3xl leading-[1.05] font-semibold tracking-tight text-fg sm:text-6xl">
|
||||
<h1
|
||||
class="font-display text-3xl leading-[1.05] font-semibold tracking-tight text-fg sm:text-6xl"
|
||||
>
|
||||
Tu música. Sin límites.
|
||||
</h1>
|
||||
</div>
|
||||
@@ -214,15 +216,15 @@
|
||||
<Rail>
|
||||
{#each albums as album, i (album.id)}
|
||||
<a
|
||||
href={album.youTubeAlbumId
|
||||
? `/albums/external/youtube/${album.youTubeAlbumId}`
|
||||
href={album.source === 'YouTube'
|
||||
? `/albums/external/youtube/${album.externalId}`
|
||||
: `/albums/${album.id}`}
|
||||
class="group/card animate-enter block w-42 shrink-0 rounded-art focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-bg focus-visible:outline-none"
|
||||
style="animation-delay:{Math.min(i, 10) * 45}ms"
|
||||
oncontextmenu={(e) => openAlbumMenu(e, album.id)}
|
||||
>
|
||||
<div class="relative">
|
||||
{#if album.thumbnailUrl}
|
||||
{#if album.source === 'YouTube' && album.thumbnailUrl}
|
||||
<Cover
|
||||
trackId={album.id}
|
||||
src={album.thumbnailUrl}
|
||||
|
||||
Reference in New Issue
Block a user