Started on media player
This commit is contained in:
@@ -40,6 +40,8 @@
|
||||
<span class="mr-0.5 text-sm">,</span>
|
||||
{/if}
|
||||
{/each}
|
||||
{:else if 'createdBy' in mediaItem && mediaItem.createdBy}
|
||||
<span class="text-sm">{mediaItem.createdBy.name}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,112 @@
|
||||
<script lang="ts">
|
||||
export let currentlyPlaying: Song
|
||||
import { onMount } from 'svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { currentlyPlaying } from '$lib/stores'
|
||||
import { FastAverageColor } from 'fast-average-color'
|
||||
import Slider from '$lib/components/util/slider.svelte'
|
||||
|
||||
export let song: Song
|
||||
|
||||
let playing = false,
|
||||
shuffle = false,
|
||||
repeat = false
|
||||
|
||||
let bgColor = 'black',
|
||||
primaryColor = 'var(--lazuli-primary)'
|
||||
|
||||
const fac = new FastAverageColor()
|
||||
$: fac.getColorAsync(`/api/remoteImage?url=${song.thumbnail}`, { algorithm: 'dominant' }).then((color) => (bgColor = color.rgb))
|
||||
$: fac.getColorAsync(`/api/remoteImage?url=${song.thumbnail}`, { algorithm: 'simple' }).then((color) => (primaryColor = color.rgb))
|
||||
// let audioSource: HTMLAudioElement, progressBar: HTMLInputElement
|
||||
// onMount(() => (audioSource.volume = 0.4))
|
||||
</script>
|
||||
|
||||
<main class="h-full w-full">
|
||||
<div class="bg-red-300">
|
||||
<audio controls>
|
||||
<source src="/api/audio?id=KfmrhlGCfWk&connectionId=cae88864-e116-4ce8-b3cc-5383eae0b781&apikey=2ff052272eeb44628c97314e09f384c10ae7fb31d8a40630442d3cb417512574" type="audio/webm" />
|
||||
</audio>
|
||||
</div>
|
||||
<div class="bg-green-300">{currentlyPlaying.type}</div>
|
||||
<main class="relative m-4 flex h-24 flex-grow-0 gap-4 overflow-clip rounded-xl text-white transition-colors duration-1000" style="background-color: {bgColor};">
|
||||
<img src={song.thumbnail} alt="" class="aspect-square max-h-full object-cover" />
|
||||
<section class="flex w-96 flex-col justify-center gap-1">
|
||||
<div class="line-clamp-2">{song.name}</div>
|
||||
<div class="text-sm text-neutral-400">{song.artists?.map((artist) => artist.name) || song.createdBy?.name}</div>
|
||||
</section>
|
||||
<section class="flex w-96 flex-col items-center justify-center gap-4">
|
||||
<div class="flex items-center gap-3 text-xl">
|
||||
<button on:click={() => (shuffle = !shuffle)} class="aspect-square h-8">
|
||||
<i class="fa-solid fa-shuffle" style="color: {shuffle ? primaryColor : 'rgb(163, 163, 163)'};" />
|
||||
</button>
|
||||
<button class="aspect-square h-8">
|
||||
<i class="fa-solid fa-backward-step" />
|
||||
</button>
|
||||
<button on:click={() => (playing = !playing)} class="grid aspect-square h-10 place-items-center rounded-full bg-white">
|
||||
<i class="fa-solid {playing ? 'fa-pause' : 'fa-play'} text-black" />
|
||||
</button>
|
||||
<button class="aspect-square h-8">
|
||||
<i class="fa-solid fa-forward-step" />
|
||||
</button>
|
||||
<button on:click={() => (repeat = !repeat)} class="aspect-square h-8">
|
||||
<i class="fa-solid fa-repeat" style="color: {repeat ? primaryColor : 'rgb(163, 163, 163)'};" />
|
||||
</button>
|
||||
</div>
|
||||
<Slider sliderColor={primaryColor} />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
<!-- <main class="h-screen w-full">
|
||||
<div class="relative h-full overflow-hidden">
|
||||
<div class="absolute z-0 flex h-full w-full items-center justify-items-center bg-neutral-900">
|
||||
<div class="absolute z-10 h-full w-full backdrop-blur-3xl"></div>
|
||||
{#key song}
|
||||
<img in:fade src={song.thumbnail} alt="" class="absolute h-full w-full object-cover brightness-50" />
|
||||
{/key}
|
||||
</div>
|
||||
<div class="absolute grid h-full w-full grid-rows-[auto_8rem_3rem_6rem] justify-items-center p-8">
|
||||
{#key song}
|
||||
<img in:fade src={song.thumbnail} alt="" class="h-full min-h-[8rem] overflow-hidden rounded-xl object-contain p-2" />
|
||||
{/key}
|
||||
<div in:fade class="flex flex-col items-center justify-center gap-1 px-8 text-center font-notoSans">
|
||||
{#key song}
|
||||
<span class="text-3xl text-neutral-300">{song.name}</span>
|
||||
{#if song.album?.name}
|
||||
<span class="text-xl text-neutral-300">{song.album.name}</span>
|
||||
{/if}
|
||||
<span class="text-xl text-neutral-300">{song.artists?.map((artist) => artist.name).join(' / ') || song.createdBy?.name}</span>
|
||||
{/key}
|
||||
</div>
|
||||
<input
|
||||
id="progress-bar"
|
||||
on:mouseup={() => (audioSource.currentTime = audioSource.duration * Number(progressBar.value))}
|
||||
type="range"
|
||||
value="0"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
class="w-[90%] cursor-pointer rounded-lg bg-gray-400"
|
||||
/>
|
||||
<div class="flex h-full w-11/12 justify-around overflow-hidden text-3xl text-white">
|
||||
<button class="relative z-0 aspect-square max-h-full max-w-full overflow-hidden rounded-full">
|
||||
<i class="fa-solid fa-backward-step" />
|
||||
</button>
|
||||
<button class="relative z-0 aspect-square max-h-full max-w-full overflow-hidden rounded-full">
|
||||
<i class={playing ? 'fa-solid fa-pause' : 'fa-solid fa-play'} />
|
||||
</button>
|
||||
<button on:click={() => ($currentlyPlaying = null)} class="relative z-0 aspect-square max-h-full max-w-full overflow-hidden rounded-full">
|
||||
<i class="fa-solid fa-stop" />
|
||||
</button>
|
||||
<button class="relative z-0 aspect-square max-h-full max-w-full overflow-hidden rounded-full">
|
||||
<i class="fa-solid fa-forward-step" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="no-scrollbar flex w-full flex-col items-center divide-y-[1px] divide-[#353535] overflow-y-scroll bg-neutral-900 p-4">
|
||||
<div>This is where playlist items go</div>
|
||||
</div>
|
||||
{#key song}
|
||||
<audio bind:this={audioSource} id="audio" class="hidden" src="/api/audio?id={song.id}&connection={song.connection}" />
|
||||
{/key}
|
||||
</main> -->
|
||||
|
||||
<!-- <style>
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
</style>
|
||||
</style> -->
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let value = 0
|
||||
export let sliderColor: string | undefined = undefined
|
||||
|
||||
let sliderThumb: HTMLSpanElement, sliderTrail: HTMLSpanElement
|
||||
|
||||
@@ -18,7 +19,8 @@
|
||||
|
||||
<div
|
||||
id="slider-track"
|
||||
class="relative isolate h-1 w-full rounded-full bg-neutral-600"
|
||||
class="relative isolate h-1.5 w-full rounded-full bg-neutral-600"
|
||||
style="--slider-color: {sliderColor || 'var(--lazuli-primary)'}"
|
||||
role="slider"
|
||||
tabindex="0"
|
||||
aria-valuenow={value}
|
||||
@@ -26,9 +28,9 @@
|
||||
aria-valuemax="100"
|
||||
on:keydown={(event) => handleKeyPress(event.key)}
|
||||
>
|
||||
<input type="range" class="absolute z-10 h-1 w-full" step="any" min="0" max="100" bind:value tabindex="-1" aria-hidden="true" aria-disabled="true" />
|
||||
<span bind:this={sliderTrail} id="slider-trail" class="absolute left-0 h-1 rounded-full bg-white transition-colors" />
|
||||
<span bind:this={sliderThumb} id="slider-thumb" class="absolute top-1/2 aspect-square h-3 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 transition-opacity duration-300" />
|
||||
<input type="range" class="absolute z-10 h-1.5 w-full" step="any" min="0" max="100" bind:value tabindex="-1" aria-hidden="true" aria-disabled="true" />
|
||||
<span bind:this={sliderTrail} id="slider-trail" class="absolute left-0 h-1.5 rounded-full bg-white transition-colors" />
|
||||
<span bind:this={sliderThumb} id="slider-thumb" class="absolute top-1/2 aspect-square h-3.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-0 transition-opacity duration-300" />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@@ -38,10 +40,10 @@
|
||||
opacity: 0;
|
||||
}
|
||||
#slider-track:hover > #slider-trail {
|
||||
background-color: var(--lazuli-primary);
|
||||
background-color: var(--slider-color);
|
||||
}
|
||||
#slider-track:focus > #slider-trail {
|
||||
background-color: var(--lazuli-primary);
|
||||
background-color: var(--slider-color);
|
||||
}
|
||||
#slider-track:hover > #slider-thumb {
|
||||
opacity: 1;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export class Jellyfin implements Connection {
|
||||
public id: string
|
||||
private userId: string
|
||||
private jfUserId: string
|
||||
private serverUrl: string
|
||||
private accessToken: string
|
||||
public readonly id: string
|
||||
private readonly userId: string
|
||||
private readonly jfUserId: string
|
||||
private readonly serverUrl: string
|
||||
private readonly accessToken: string
|
||||
|
||||
private readonly BASEHEADERS: Headers
|
||||
private readonly authHeader: Headers
|
||||
|
||||
constructor(id: string, userId: string, jellyfinUserId: string, serverUrl: string, accessToken: string) {
|
||||
this.id = id
|
||||
@@ -14,33 +14,34 @@ export class Jellyfin implements Connection {
|
||||
this.serverUrl = serverUrl
|
||||
this.accessToken = accessToken
|
||||
|
||||
this.BASEHEADERS = new Headers({ Authorization: `MediaBrowser Token="${this.accessToken}"` })
|
||||
this.authHeader = new Headers({ Authorization: `MediaBrowser Token="${this.accessToken}"` })
|
||||
}
|
||||
|
||||
// const audoSearchParams = new URLSearchParams({
|
||||
// MaxStreamingBitrate: '999999999',
|
||||
// Container: 'opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg',
|
||||
// TranscodingContainer: 'ts',
|
||||
// TranscodingProtocol: 'hls',
|
||||
// AudioCodec: 'aac',
|
||||
// userId: this.jfUserId,
|
||||
// })
|
||||
|
||||
public getConnectionInfo = async (): Promise<Extract<ConnectionInfo, { type: 'jellyfin' }>> => {
|
||||
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl).toString()
|
||||
const systemUrl = new URL('System/Info', this.serverUrl).toString()
|
||||
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl)
|
||||
const systemUrl = new URL('System/Info', this.serverUrl)
|
||||
|
||||
const userData: JellyfinAPI.User = await fetch(userUrl, { headers: this.BASEHEADERS }).then((response) => response.json())
|
||||
const systemData: JellyfinAPI.System = await fetch(systemUrl, { headers: this.BASEHEADERS }).then((response) => response.json())
|
||||
const userData: JellyfinAPI.User | undefined = await fetch(userUrl, { headers: this.authHeader })
|
||||
.then((response) => response.json())
|
||||
.catch(() => {
|
||||
console.error(`Fetch to ${userUrl.toString()} failed`)
|
||||
return undefined
|
||||
})
|
||||
const systemData: JellyfinAPI.System | undefined = await fetch(systemUrl, { headers: this.authHeader })
|
||||
.then((response) => response.json())
|
||||
.catch(() => {
|
||||
console.error(`Fetch to ${systemUrl.toString()} failed`)
|
||||
return undefined
|
||||
})
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
type: 'jellyfin',
|
||||
serverUrl: this.serverUrl,
|
||||
serverName: systemData.ServerName,
|
||||
serverName: systemData?.ServerName,
|
||||
jellyfinUserId: this.jfUserId,
|
||||
username: userData.Name,
|
||||
username: userData?.Name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,16 +54,11 @@ export class Jellyfin implements Connection {
|
||||
limit: '10',
|
||||
})
|
||||
|
||||
const mostPlayedSongsURL = new URL(`/Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl).toString()
|
||||
const mostPlayedSongsURL = new URL(`/Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||
|
||||
const mostPlayedResponse = await fetch(mostPlayedSongsURL, { headers: this.BASEHEADERS })
|
||||
const mostPlayedData = await mostPlayedResponse.json()
|
||||
const mostPlayed: { Items: JellyfinAPI.Song[] } = await fetch(mostPlayedSongsURL, { headers: this.authHeader }).then((response) => response.json())
|
||||
|
||||
return Array.from(mostPlayedData.Items as JellyfinAPI.Song[], (song) => this.parseSong(song))
|
||||
}
|
||||
|
||||
public getSongAudio = (id: string): string => {
|
||||
return 'need to implement'
|
||||
return Array.from(mostPlayed.Items, (song) => this.parseSong(song))
|
||||
}
|
||||
|
||||
public search = async (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist'): Promise<(Song | Album | Playlist)[]> => {
|
||||
@@ -72,9 +68,9 @@ export class Jellyfin implements Connection {
|
||||
recursive: 'true',
|
||||
})
|
||||
|
||||
const searchURL = new URL(`Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl).toString()
|
||||
const searchResponse = await fetch(searchURL, { headers: this.BASEHEADERS })
|
||||
if (!searchResponse.ok) throw new JellyfinFetchError('Failed to search Jellyfin', searchResponse.status, searchURL)
|
||||
const searchURL = new URL(`Users/${this.jfUserId}/Items?${searchParams.toString()}`, this.serverUrl)
|
||||
const searchResponse = await fetch(searchURL, { headers: this.authHeader })
|
||||
if (!searchResponse.ok) throw new JellyfinFetchError('Failed to search Jellyfin', searchResponse.status, searchURL.toString())
|
||||
const searchResults = (await searchResponse.json()).Items as (JellyfinAPI.Song | JellyfinAPI.Album | JellyfinAPI.Playlist)[] // JellyfinAPI.Artist
|
||||
|
||||
const parsedResults: (Song | Album | Playlist)[] = Array.from(searchResults, (result) => {
|
||||
@@ -90,6 +86,21 @@ export class Jellyfin implements Connection {
|
||||
return parsedResults
|
||||
}
|
||||
|
||||
public getAudioStream = async (id: string): Promise<Response> => {
|
||||
const audoSearchParams = new URLSearchParams({
|
||||
MaxStreamingBitrate: '2000000',
|
||||
Container: 'opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg',
|
||||
TranscodingContainer: 'ts',
|
||||
TranscodingProtocol: 'hls',
|
||||
AudioCodec: 'aac',
|
||||
userId: this.jfUserId,
|
||||
})
|
||||
|
||||
const audioUrl = new URL(`Audio/${id}/universal?${audoSearchParams.toString()}`, this.serverUrl)
|
||||
|
||||
return await fetch(audioUrl, { headers: this.authHeader })
|
||||
}
|
||||
|
||||
private parseSong = (song: JellyfinAPI.Song): Song => {
|
||||
const thumbnail = song.ImageTags?.Primary
|
||||
? new URL(`Items/${song.Id}/Images/Primary`, this.serverUrl).href
|
||||
|
||||
@@ -156,29 +156,34 @@ export class YouTubeMusic implements Connection {
|
||||
return recommendations
|
||||
}
|
||||
|
||||
public getSongAudio = async (id: string): Promise<ReadableStream<Uint8Array>> => {
|
||||
public getAudioStream = async (id: string): Promise<Response> => {
|
||||
const videoInfo = await ytdl.getInfo(`http://www.youtube.com/watch?v=${id}`)
|
||||
const format = ytdl.chooseFormat(videoInfo.formats, { quality: 'highestaudio', filter: 'audioonly' })
|
||||
const audioResponse = await fetch(format.url)
|
||||
return audioResponse.body!
|
||||
|
||||
return await fetch(format.url)
|
||||
}
|
||||
}
|
||||
|
||||
const parseTwoRowItemRenderer = (connection: string, rowContent: InnerTube.musicTwoRowItemRenderer): Song | Album | Artist | Playlist => {
|
||||
const name = rowContent.title.runs[0].text
|
||||
|
||||
let artists: (Song | Album)['artists']
|
||||
let artists: (Song | Album)['artists'], createdBy: (Song | Playlist)['createdBy']
|
||||
for (const run of rowContent.subtitle.runs) {
|
||||
if (!run.navigationEndpoint) continue
|
||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
artists ? artists.push(artist) : (artists = [artist])
|
||||
const pageType = run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||
if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
artists ? artists.push(artist) : (artists = [artist])
|
||||
} else if (pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL') {
|
||||
createdBy = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
}
|
||||
}
|
||||
|
||||
const thumbnail = refineThumbnailUrl(rowContent.thumbnailRenderer.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||
|
||||
if ('watchEndpoint' in rowContent.navigationEndpoint) {
|
||||
const id = rowContent.navigationEndpoint.watchEndpoint.videoId
|
||||
return { connection, id, name, type: 'song', artists, thumbnail } satisfies Song
|
||||
return { connection, id, name, type: 'song', artists, createdBy, thumbnail } satisfies Song
|
||||
}
|
||||
|
||||
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||
@@ -187,9 +192,10 @@ const parseTwoRowItemRenderer = (connection: string, rowContent: InnerTube.music
|
||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||
return { connection, id, name, type: 'playlist', createdBy, thumbnail } satisfies Playlist
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,11 +203,16 @@ const parseResponsiveListItemRenderer = (connection: string, listContent: InnerT
|
||||
const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text
|
||||
const thumbnail = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||
|
||||
let artists: (Song | Album)['artists']
|
||||
let artists: (Song | Album)['artists'], createdBy: (Song | Playlist)['createdBy']
|
||||
for (const run of listContent.flexColumns[1].musicResponsiveListItemFlexColumnRenderer.text.runs) {
|
||||
if (!run.navigationEndpoint) continue
|
||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
artists ? artists.push(artist) : (artists = [artist])
|
||||
const pageType = run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||
if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
artists ? artists.push(artist) : (artists = [artist])
|
||||
} else if (pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL') {
|
||||
createdBy = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
}
|
||||
}
|
||||
|
||||
if (!('navigationEndpoint' in listContent)) {
|
||||
@@ -212,7 +223,7 @@ const parseResponsiveListItemRenderer = (connection: string, listContent: InnerT
|
||||
album = { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text }
|
||||
}
|
||||
|
||||
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
|
||||
return { connection, id, name, type: 'song', artists, album, createdBy, thumbnail } satisfies Song
|
||||
}
|
||||
|
||||
const id = listContent.navigationEndpoint.browseEndpoint.browseId
|
||||
@@ -221,9 +232,10 @@ const parseResponsiveListItemRenderer = (connection: string, listContent: InnerT
|
||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||
return { connection, id, name, type: 'playlist', createdBy, thumbnail } satisfies Playlist
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,23 +243,25 @@ const parseMusicCardShelfRenderer = (connection: string, cardContent: InnerTube.
|
||||
const name = cardContent.title.runs[0].text
|
||||
const thumbnail = refineThumbnailUrl(cardContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
||||
|
||||
let album: Song['album'], artists: (Song | Album)['artists']
|
||||
let album: Song['album'], artists: (Song | Album)['artists'], createdBy: (Song | Playlist)['createdBy']
|
||||
for (const run of cardContent.subtitle.runs) {
|
||||
if (!run.navigationEndpoint) continue
|
||||
|
||||
const pageType = run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||
if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||
if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
||||
album = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
} else if (pageType === 'MUSIC_PAGE_TYPE_ARTIST') {
|
||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
artists ? artists.push(artist) : (artists = [artist])
|
||||
} else if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
||||
album = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
} else if (pageType === 'MUSIC_PAGE_TYPE_USER_CHANNEL') {
|
||||
createdBy = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||
}
|
||||
}
|
||||
|
||||
const navigationEndpoint = cardContent.title.runs[0].navigationEndpoint
|
||||
if ('watchEndpoint' in navigationEndpoint) {
|
||||
const id = navigationEndpoint.watchEndpoint.videoId
|
||||
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
|
||||
return { connection, id, name, type: 'song', artists, album, createdBy, thumbnail } satisfies Song
|
||||
}
|
||||
|
||||
const pageType = navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||
@@ -256,9 +270,10 @@ const parseMusicCardShelfRenderer = (connection: string, cardContent: InnerTube.
|
||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||
case 'MUSIC_PAGE_TYPE_USER_CHANNEL':
|
||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||
return { connection, id, name, type: 'playlist', createdBy, thumbnail } satisfies Playlist
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,13 +281,18 @@ const refineThumbnailUrl = (urlString: string): string => {
|
||||
if (!URL.canParse(urlString)) throw new Error('Invalid thumbnail url')
|
||||
|
||||
const url = new URL(urlString)
|
||||
if (url.origin === 'https://i.ytimg.com') {
|
||||
return urlString.slice(0, urlString.indexOf('?')).replace('sddefault', 'mqdefault')
|
||||
} else if (url.origin === 'https://lh3.googleusercontent.com' || url.origin === 'https://yt3.googleusercontent.com' || url.origin === 'https://yt3.ggpht.com') {
|
||||
return urlString.slice(0, urlString.indexOf('='))
|
||||
} else {
|
||||
console.error(urlString)
|
||||
throw new Error('Invalid thumbnail url origin')
|
||||
switch (url.origin) {
|
||||
case 'https://i.ytimg.com':
|
||||
return urlString.slice(0, urlString.indexOf('?')).replace('sddefault', 'mqdefault')
|
||||
case 'https://lh3.googleusercontent.com':
|
||||
case 'https://yt3.googleusercontent.com':
|
||||
case 'https://yt3.ggpht.com':
|
||||
return urlString.slice(0, urlString.indexOf('='))
|
||||
case 'https://music.youtube.com':
|
||||
return urlString
|
||||
default:
|
||||
console.error(urlString)
|
||||
throw new Error('Invalid thumbnail url origin')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -520,7 +540,7 @@ declare namespace InnerTube {
|
||||
browseId: string
|
||||
browseEndpointContextSupportedConfigs: {
|
||||
browseEndpointContextMusicConfig: {
|
||||
pageType: 'MUSIC_PAGE_TYPE_ALBUM' | 'MUSIC_PAGE_TYPE_ARTIST' | 'MUSIC_PAGE_TYPE_PLAYLIST'
|
||||
pageType: 'MUSIC_PAGE_TYPE_ALBUM' | 'MUSIC_PAGE_TYPE_ARTIST' | 'MUSIC_PAGE_TYPE_PLAYLIST' | 'MUSIC_PAGE_TYPE_USER_CHANNEL'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user