feat(web-player): show YouTube Music albums in the explore search
The Albums chip now returns both your own albums and what YouTube Music has for the term. A YouTube card opens a read-only album page with its cover, tracklist and play button, and right-clicking a track still offers to add it to a playlist through the existing menu.
This commit is contained in:
Vendored
+140
@@ -454,6 +454,40 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/youtube/albums": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Search Albums On YouTube Music. */
|
||||
get: operations["SearchYouTubeAlbums"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/youtube/albums/{albumId}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get A YouTube Music Album With Its Tracklist. */
|
||||
get: operations["GetYouTubeAlbum"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/youtube/tracks/{videoId}/stream": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
@@ -696,6 +730,37 @@ export interface components {
|
||||
/** Format: date-time */
|
||||
updatedAt: string;
|
||||
};
|
||||
YouTubeAlbumDetail: {
|
||||
album: components["schemas"]["YouTubeAlbumResult"];
|
||||
description: string;
|
||||
/** Format: int32 */
|
||||
totalDurationSeconds: number | string;
|
||||
tracks: components["schemas"]["YouTubeAlbumTrack"][];
|
||||
};
|
||||
YouTubeAlbumResult: {
|
||||
albumId: string;
|
||||
title: string;
|
||||
artist: string;
|
||||
thumbnailUrl: string;
|
||||
/** Format: int32 */
|
||||
releaseYear: null | number | string;
|
||||
isSingle: boolean;
|
||||
isEp: boolean;
|
||||
artists: components["schemas"]["YouTubeArtistRef"][];
|
||||
};
|
||||
YouTubeAlbumSearchResult: {
|
||||
items: components["schemas"]["YouTubeAlbumResult"][];
|
||||
continuationToken: string;
|
||||
};
|
||||
YouTubeAlbumTrack: {
|
||||
videoId: string;
|
||||
title: string;
|
||||
/** Format: int32 */
|
||||
durationSeconds: number | string;
|
||||
/** Format: int32 */
|
||||
trackNumber: number | string;
|
||||
isExplicit: boolean;
|
||||
};
|
||||
YouTubeArtistRef: {
|
||||
id: null | string;
|
||||
name: string;
|
||||
@@ -1962,6 +2027,81 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
SearchYouTubeAlbums: {
|
||||
parameters: {
|
||||
query: {
|
||||
query: string;
|
||||
continuation?: string;
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["YouTubeAlbumSearchResult"];
|
||||
};
|
||||
};
|
||||
/** @description Bad Request */
|
||||
400: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/problem+json": components["schemas"]["HttpValidationProblemDetails"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
GetYouTubeAlbum: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
albumId: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description OK */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["YouTubeAlbumDetail"];
|
||||
};
|
||||
};
|
||||
/** @description Unauthorized */
|
||||
401: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
/** @description Not Found */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
GetYouTubeTrackStream: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
|
||||
@@ -1,35 +1,68 @@
|
||||
<script lang="ts">
|
||||
import PlaylistArt from './PlaylistArt.svelte';
|
||||
import Cover from './Cover.svelte';
|
||||
import { hueFor } from '$lib/theme/color';
|
||||
|
||||
interface Props {
|
||||
id: string;
|
||||
title: string;
|
||||
href?: string;
|
||||
subtitle?: string;
|
||||
releaseYear?: number;
|
||||
trackCount: number;
|
||||
trackIds: (string | number)[];
|
||||
trackCount?: number;
|
||||
trackIds?: (string | number)[];
|
||||
coverSrc?: string | null;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
let { id, title, releaseYear, trackCount, trackIds, index = 0 }: Props = $props();
|
||||
let {
|
||||
id,
|
||||
title,
|
||||
href,
|
||||
subtitle,
|
||||
releaseYear,
|
||||
trackCount,
|
||||
trackIds = [],
|
||||
coverSrc,
|
||||
index = 0
|
||||
}: Props = $props();
|
||||
|
||||
const meta = $derived(
|
||||
[releaseYear, `${trackCount} ${trackCount === 1 ? 'canción' : 'canciones'}`]
|
||||
.filter((part) => part !== undefined && part !== null)
|
||||
[
|
||||
subtitle,
|
||||
releaseYear ? String(releaseYear) : undefined,
|
||||
trackCount === undefined
|
||||
? undefined
|
||||
: `${trackCount} ${trackCount === 1 ? 'canción' : 'canciones'}`
|
||||
]
|
||||
.filter((part) => !!part)
|
||||
.join(' · ')
|
||||
);
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/albums/{id}"
|
||||
href={href ?? `/albums/${id}`}
|
||||
class="group animate-enter min-w-0 text-left"
|
||||
style="animation-delay:{index * 45}ms"
|
||||
>
|
||||
<PlaylistArt
|
||||
{trackIds}
|
||||
hue={hueFor(id)}
|
||||
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
|
||||
/>
|
||||
{#if coverSrc}
|
||||
<Cover
|
||||
trackId={id}
|
||||
src={coverSrc}
|
||||
hue={hueFor(id)}
|
||||
size="large"
|
||||
alt={title}
|
||||
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
|
||||
/>
|
||||
{:else}
|
||||
<PlaylistArt
|
||||
{trackIds}
|
||||
hue={hueFor(id)}
|
||||
class="aspect-square w-full rounded-art shadow-art transition group-hover:shadow-art-lg"
|
||||
/>
|
||||
{/if}
|
||||
<div class="mt-2.5 truncate text-fg">{title}</div>
|
||||
<div class="mt-0.5 truncate text-sm text-fg-3">{meta}</div>
|
||||
{#if meta}
|
||||
<div class="mt-0.5 truncate text-sm text-fg-3">{meta}</div>
|
||||
{/if}
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user