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.
Removes the last YouTube-specific routes (/youtube/*) and merges them
into the general track/album/playlist endpoints, keyed by a `source`
string validated against TrackSource. This mirrors the earlier track
search merge for albums and playlist-track additions:
- GetAlbums now merges local albums with live YouTube Music search
results, paginated via a youtubeContinuationToken, deduped against
already-materialized ExternalAlbum rows.
- New GET /albums/external/{source}/{externalId} and
GET /tracks/external/{source}/{externalId}/stream replace the
YouTube-only equivalents.
- POST /playlists/{id}/tracks replaces the separate
.../youtube-tracks endpoint, accepting an optional Source/ExternalId
pair alongside TrackId.
- AlbumSearchItemResponse/AlbumsSearchResponse follow the same
discriminated-union + trailing-HasNextPage-field pattern already
used for track search, so HasNextPage stays a required field in the
generated OpenAPI schema.
- Frontend routes renamed accordingly (/albums/youtube/[albumId] ->
/albums/external/[source]/[externalId], etc.) with a single
/api/thumbnail proxy replacing /api/youtube/thumbnail.
Musify.Api.Endpoints.YouTubeEndpoints and the standalone
SearchYouTubeAlbums query are removed as a result.