Redoing some of the types
This commit is contained in:
66
src/app.d.ts
vendored
66
src/app.d.ts
vendored
@@ -24,22 +24,6 @@ declare global {
|
|||||||
passwordHash: string
|
passwordHash: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConnectionInfo = {
|
|
||||||
id: string
|
|
||||||
userId: string
|
|
||||||
} & (
|
|
||||||
| {
|
|
||||||
type: 'jellyfin'
|
|
||||||
serviceInfo: Jellyfin.SerivceInfo
|
|
||||||
tokens: Jellyfin.Tokens
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'youtube-music'
|
|
||||||
serviceInfo: YouTubeMusic.SerivceInfo
|
|
||||||
tokens: YouTubeMusic.Tokens
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
interface Connection {
|
interface Connection {
|
||||||
public id: string
|
public id: string
|
||||||
getRecommendations: () => Promise<(Song | Album | Artist | Playlist)[]>
|
getRecommendations: () => Promise<(Song | Album | Artist | Playlist)[]>
|
||||||
@@ -51,10 +35,7 @@ declare global {
|
|||||||
// Big data should be fetched as needed in the app, these exist to ensure that the info necessary to fetch that data is there.
|
// Big data should be fetched as needed in the app, these exist to ensure that the info necessary to fetch that data is there.
|
||||||
|
|
||||||
type Song = {
|
type Song = {
|
||||||
connection: {
|
connection: string
|
||||||
id: string
|
|
||||||
type: 'jellyfin' | 'youtube-music'
|
|
||||||
}
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'song'
|
type: 'song'
|
||||||
@@ -74,10 +55,7 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Album = {
|
type Album = {
|
||||||
connection: {
|
connection: string
|
||||||
id: string
|
|
||||||
type: 'jellyfin' | 'youtube-music'
|
|
||||||
}
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'album'
|
type: 'album'
|
||||||
@@ -93,10 +71,7 @@ declare global {
|
|||||||
|
|
||||||
// Need to figure out how to do Artists, maybe just query MusicBrainz?
|
// Need to figure out how to do Artists, maybe just query MusicBrainz?
|
||||||
type Artist = {
|
type Artist = {
|
||||||
connection: {
|
connection: string
|
||||||
id: string
|
|
||||||
type: 'jellyfin' | 'youtube-music'
|
|
||||||
}
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'artist'
|
type: 'artist'
|
||||||
@@ -104,46 +79,13 @@ declare global {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Playlist = {
|
type Playlist = {
|
||||||
connection: {
|
connection: string
|
||||||
id: string
|
|
||||||
type: 'jellyfin' | 'youtube-music'
|
|
||||||
}
|
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
type: 'playlist'
|
type: 'playlist'
|
||||||
thumbnail?: string
|
thumbnail?: string
|
||||||
description?: string
|
description?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
namespace Jellyfin {
|
|
||||||
// The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will
|
|
||||||
// retrun the ServerName, it wont. This must be fetched from /System/Info.
|
|
||||||
// So, ONLY DEFINE THE INTERFACES FOR DATA THAT IS GARUNTEED TO BE RETURNED (unless the data value itself is inherently optional)
|
|
||||||
type SerivceInfo = {
|
|
||||||
userId: string
|
|
||||||
urlOrigin: string
|
|
||||||
username?: string
|
|
||||||
serverName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tokens = {
|
|
||||||
accessToken: string
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace YouTubeMusic {
|
|
||||||
type SerivceInfo = {
|
|
||||||
userId: string
|
|
||||||
username?: string
|
|
||||||
profilePicture?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Tokens = {
|
|
||||||
accessToken: string
|
|
||||||
refreshToken: string
|
|
||||||
expiry: number
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {}
|
export {}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
{#if 'artists' in mediaItem && mediaItem.artists}
|
{#if 'artists' in mediaItem && mediaItem.artists}
|
||||||
{#each mediaItem.artists as artist}
|
{#each mediaItem.artists as artist}
|
||||||
{@const listIndex = mediaItem.artists.indexOf(artist)}
|
{@const listIndex = mediaItem.artists.indexOf(artist)}
|
||||||
<a class="text-sm hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={mediaItem.connection.id}">{artist.name}</a>
|
<a class="text-sm hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={mediaItem.connection}">{artist.name}</a>
|
||||||
{#if listIndex < mediaItem.artists.length - 1}
|
{#if listIndex < mediaItem.artists.length - 1}
|
||||||
<span class="mr-0.5 text-sm">,</span>
|
<span class="mr-0.5 text-sm">,</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
import { DB, type DBConnectionInfo } from './db'
|
import { DB, type DBConnectionInfo } from './db'
|
||||||
import { Jellyfin } from './jellyfin'
|
import { Jellyfin, type JellyfinConnectionInfo } from './jellyfin'
|
||||||
import { YouTubeMusic } from './youtube-music'
|
import { YouTubeMusic, type YouTubeMusicConnectionInfo } from './youtube-music'
|
||||||
|
|
||||||
|
export type ConnectionInfo = JellyfinConnectionInfo | YouTubeMusicConnectionInfo
|
||||||
|
|
||||||
const constructConnection = (connectionInfo: DBConnectionInfo): Connection => {
|
const constructConnection = (connectionInfo: DBConnectionInfo): Connection => {
|
||||||
const { id, userId, type, serviceInfo, tokens } = connectionInfo
|
const { id, userId, type, service, tokens } = connectionInfo
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'jellyfin':
|
case 'jellyfin':
|
||||||
return new Jellyfin(id, userId, serviceInfo.userId, serviceInfo.urlOrigin, tokens.accessToken)
|
return new Jellyfin(id, userId, service.userId, service.urlOrigin, tokens.accessToken)
|
||||||
case 'youtube-music':
|
case 'youtube-music':
|
||||||
return new YouTubeMusic(id, userId, serviceInfo.userId, tokens)
|
return new YouTubeMusic(id, userId, service.userId, tokens)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,22 +10,34 @@ interface DBConnectionsTableSchema {
|
|||||||
tokens?: string
|
tokens?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DBServiceInfo =
|
type JellyfinDBConnection = {
|
||||||
| {
|
|
||||||
type: 'jellyfin'
|
|
||||||
serviceInfo: Pick<Jellyfin.SerivceInfo, 'userId' | 'urlOrigin'>
|
|
||||||
tokens: Jellyfin.Tokens
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
type: 'youtube-music'
|
|
||||||
serviceInfo: Pick<YouTubeMusic.SerivceInfo, 'userId'>
|
|
||||||
tokens: YouTubeMusic.Tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
export type DBConnectionInfo = {
|
|
||||||
id: string
|
id: string
|
||||||
userId: string
|
userId: string
|
||||||
} & DBServiceInfo
|
type: 'jellyfin'
|
||||||
|
service: {
|
||||||
|
userId: string
|
||||||
|
urlOrigin: string
|
||||||
|
}
|
||||||
|
tokens: {
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type YouTubeMusicDBConnection = {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
type: 'youtube-music'
|
||||||
|
service: {
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
tokens: {
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
expiry: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DBConnectionInfo = JellyfinDBConnection | YouTubeMusicDBConnection
|
||||||
|
|
||||||
class Storage {
|
class Storage {
|
||||||
private readonly database: Sqlite3DB
|
private readonly database: Sqlite3DB
|
||||||
@@ -86,7 +98,7 @@ class Storage {
|
|||||||
const { userId, type, service, tokens } = result
|
const { userId, type, service, tokens } = result
|
||||||
const parsedService = service ? JSON.parse(service) : undefined
|
const parsedService = service ? JSON.parse(service) : undefined
|
||||||
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
||||||
connectionInfo.push({ id, userId, type: type as DBServiceInfo['type'], serviceInfo: parsedService, tokens: parsedTokens })
|
connectionInfo.push({ id, userId, type: type as DBConnectionInfo['type'], service: parsedService, tokens: parsedTokens })
|
||||||
}
|
}
|
||||||
return connectionInfo
|
return connectionInfo
|
||||||
}
|
}
|
||||||
@@ -97,15 +109,15 @@ class Storage {
|
|||||||
for (const { id, type, service, tokens } of connectionRows) {
|
for (const { id, type, service, tokens } of connectionRows) {
|
||||||
const parsedService = service ? JSON.parse(service) : undefined
|
const parsedService = service ? JSON.parse(service) : undefined
|
||||||
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
||||||
connections.push({ id, userId, type: type as DBServiceInfo['type'], serviceInfo: parsedService, tokens: parsedTokens })
|
connections.push({ id, userId, type: type as DBConnectionInfo['type'], service: parsedService, tokens: parsedTokens })
|
||||||
}
|
}
|
||||||
return connections
|
return connections
|
||||||
}
|
}
|
||||||
|
|
||||||
public addConnectionInfo = (userId: string, serviceData: DBServiceInfo): string => {
|
public addConnectionInfo = (connectionInfo: Omit<DBConnectionInfo, 'id'>): string => {
|
||||||
const { type, serviceInfo, tokens } = serviceData
|
const { userId, type, service, tokens } = connectionInfo
|
||||||
const connectionId = generateUUID()
|
const connectionId = generateUUID()
|
||||||
this.database.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, JSON.stringify(serviceInfo), JSON.stringify(tokens))
|
this.database.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, JSON.stringify(service), JSON.stringify(tokens))
|
||||||
return connectionId
|
return connectionId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,18 @@
|
|||||||
|
export type JellyfinConnectionInfo = {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
type: 'jellyfin'
|
||||||
|
service: {
|
||||||
|
userId: string
|
||||||
|
serverUrl: string
|
||||||
|
username: string
|
||||||
|
serverName: string
|
||||||
|
}
|
||||||
|
tokens: {
|
||||||
|
accessToken: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class Jellyfin implements Connection {
|
export class Jellyfin implements Connection {
|
||||||
public id: string
|
public id: string
|
||||||
private userId: string
|
private userId: string
|
||||||
@@ -26,7 +41,7 @@ export class Jellyfin implements Connection {
|
|||||||
// userId: this.jfUserId,
|
// userId: this.jfUserId,
|
||||||
// })
|
// })
|
||||||
|
|
||||||
public getConnectionInfo = async (): Promise<Extract<ConnectionInfo, { type: 'jellyfin' }>> => {
|
public getConnectionInfo = async (): Promise<JellyfinConnectionInfo> => {
|
||||||
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl).href
|
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl).href
|
||||||
const systemUrl = new URL('System/Info', this.serverUrl).href
|
const systemUrl = new URL('System/Info', this.serverUrl).href
|
||||||
|
|
||||||
@@ -40,9 +55,9 @@ export class Jellyfin implements Connection {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
type: 'jellyfin',
|
type: 'jellyfin',
|
||||||
serviceInfo: {
|
service: {
|
||||||
userId: this.jfUserId,
|
userId: this.jfUserId,
|
||||||
urlOrigin: this.serverUrl,
|
serverUrl: this.serverUrl,
|
||||||
username: userData.Name,
|
username: userData.Name,
|
||||||
serverName: systemData.ServerName,
|
serverName: systemData.ServerName,
|
||||||
},
|
},
|
||||||
@@ -110,10 +125,7 @@ export class Jellyfin implements Connection {
|
|||||||
const album: Song['album'] = song.AlbumId && song.Album ? { id: song.AlbumId, name: song.Album } : undefined
|
const album: Song['album'] = song.AlbumId && song.Album ? { id: song.AlbumId, name: song.Album } : undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connection: {
|
connection: this.id,
|
||||||
id: this.id,
|
|
||||||
type: 'jellyfin',
|
|
||||||
},
|
|
||||||
type: 'song',
|
type: 'song',
|
||||||
id: song.Id,
|
id: song.Id,
|
||||||
name: song.Name,
|
name: song.Name,
|
||||||
@@ -135,10 +147,7 @@ export class Jellyfin implements Connection {
|
|||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connection: {
|
connection: this.id,
|
||||||
id: this.id,
|
|
||||||
type: 'jellyfin',
|
|
||||||
},
|
|
||||||
type: 'album',
|
type: 'album',
|
||||||
id: album.Id,
|
id: album.Id,
|
||||||
name: album.Name,
|
name: album.Name,
|
||||||
@@ -153,10 +162,7 @@ export class Jellyfin implements Connection {
|
|||||||
const thumbnail = playlist.ImageTags?.Primary ? new URL(`Items/${playlist.Id}/Images/Primary`, this.serverUrl).toString() : undefined
|
const thumbnail = playlist.ImageTags?.Primary ? new URL(`Items/${playlist.Id}/Images/Primary`, this.serverUrl).toString() : undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
connection: {
|
connection: this.id,
|
||||||
id: this.id,
|
|
||||||
type: 'jellyfin',
|
|
||||||
},
|
|
||||||
id: playlist.Id,
|
id: playlist.Id,
|
||||||
name: playlist.Name,
|
name: playlist.Name,
|
||||||
type: 'playlist',
|
type: 'playlist',
|
||||||
|
|||||||
@@ -3,13 +3,29 @@ import { DB } from './db'
|
|||||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||||
import { YOUTUBE_API_CLIENT_SECRET } from '$env/static/private'
|
import { YOUTUBE_API_CLIENT_SECRET } from '$env/static/private'
|
||||||
|
|
||||||
|
export type YouTubeMusicConnectionInfo = {
|
||||||
|
id: string
|
||||||
|
userId: string
|
||||||
|
type: 'youtube-music'
|
||||||
|
service: {
|
||||||
|
userId: string
|
||||||
|
username: string
|
||||||
|
profilePicture: string
|
||||||
|
}
|
||||||
|
tokens: {
|
||||||
|
accessToken: string
|
||||||
|
refreshToken: string
|
||||||
|
expiry: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class YouTubeMusic implements Connection {
|
export class YouTubeMusic implements Connection {
|
||||||
public id: string
|
public id: string
|
||||||
private userId: string
|
private userId: string
|
||||||
private ytUserId: string
|
private ytUserId: string
|
||||||
private tokens: YouTubeMusic.Tokens
|
private tokens: YouTubeMusicConnectionInfo['tokens']
|
||||||
|
|
||||||
constructor(id: string, userId: string, youtubeUserId: string, tokens: YouTubeMusic.Tokens) {
|
constructor(id: string, userId: string, youtubeUserId: string, tokens: YouTubeMusicConnectionInfo['tokens']) {
|
||||||
this.id = id
|
this.id = id
|
||||||
this.userId = userId
|
this.userId = userId
|
||||||
this.ytUserId = youtubeUserId
|
this.ytUserId = youtubeUserId
|
||||||
@@ -30,7 +46,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTokens = async (): Promise<YouTubeMusic.Tokens> => {
|
private getTokens = async (): Promise<YouTubeMusicConnectionInfo['tokens']> => {
|
||||||
if (this.tokens.expiry < Date.now()) {
|
if (this.tokens.expiry < Date.now()) {
|
||||||
const refreshToken = this.tokens.refreshToken
|
const refreshToken = this.tokens.refreshToken
|
||||||
|
|
||||||
@@ -47,7 +63,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
const { access_token, expires_in } = await response.json()
|
const { access_token, expires_in } = await response.json()
|
||||||
const newExpiry = Date.now() + expires_in * 1000
|
const newExpiry = Date.now() + expires_in * 1000
|
||||||
|
|
||||||
const newTokens: YouTubeMusic.Tokens = { accessToken: access_token, refreshToken, expiry: newExpiry }
|
const newTokens: YouTubeMusicConnectionInfo['tokens'] = { accessToken: access_token, refreshToken, expiry: newExpiry }
|
||||||
DB.updateTokens(this.id, newTokens)
|
DB.updateTokens(this.id, newTokens)
|
||||||
this.tokens = newTokens
|
this.tokens = newTokens
|
||||||
}
|
}
|
||||||
@@ -55,7 +71,7 @@ export class YouTubeMusic implements Connection {
|
|||||||
return this.tokens
|
return this.tokens
|
||||||
}
|
}
|
||||||
|
|
||||||
public getConnectionInfo = async (): Promise<Extract<ConnectionInfo, { type: 'youtube-music' }>> => {
|
public getConnectionInfo = async (): Promise<YouTubeMusicConnectionInfo> => {
|
||||||
const youtube = google.youtube('v3')
|
const youtube = google.youtube('v3')
|
||||||
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['snippet'], access_token: (await this.getTokens()).accessToken })
|
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['snippet'], access_token: (await this.getTokens()).accessToken })
|
||||||
const userChannel = userChannelResponse.data.items![0]
|
const userChannel = userChannelResponse.data.items![0]
|
||||||
@@ -64,10 +80,10 @@ export class YouTubeMusic implements Connection {
|
|||||||
id: this.id,
|
id: this.id,
|
||||||
userId: this.userId,
|
userId: this.userId,
|
||||||
type: 'youtube-music',
|
type: 'youtube-music',
|
||||||
serviceInfo: {
|
service: {
|
||||||
userId: this.ytUserId,
|
userId: this.ytUserId,
|
||||||
username: userChannel.snippet?.title as string,
|
username: userChannel.snippet?.title as string,
|
||||||
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined,
|
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string,
|
||||||
},
|
},
|
||||||
tokens: await this.getTokens(),
|
tokens: await this.getTokens(),
|
||||||
}
|
}
|
||||||
@@ -104,16 +120,20 @@ export class YouTubeMusic implements Connection {
|
|||||||
const contents = searchResulsts.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
|
const contents = searchResulsts.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
|
||||||
|
|
||||||
const parsedSearchResults: (Song | Album | Artist | Playlist)[] = []
|
const parsedSearchResults: (Song | Album | Artist | Playlist)[] = []
|
||||||
|
const goodSections = ['Songs', 'Videos', 'Albums', 'Artists', 'Community playlists']
|
||||||
for (const section of contents) {
|
for (const section of contents) {
|
||||||
if ('musicCardShelfRenderer' in section) {
|
if ('musicCardShelfRenderer' in section) {
|
||||||
parsedSearchResults.push(this.parseMusicCardShelfRenderer(section.musicCardShelfRenderer))
|
parsedSearchResults.push(parseMusicCardShelfRenderer(this.id, section.musicCardShelfRenderer))
|
||||||
|
section.musicCardShelfRenderer.contents?.forEach((item) => {
|
||||||
|
parsedSearchResults.push(parseResponsiveListItemRenderer(this.id, item.musicResponsiveListItemRenderer))
|
||||||
|
})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const sectionType = section.musicShelfRenderer.title.runs[0].text
|
const sectionType = section.musicShelfRenderer.title.runs[0].text
|
||||||
if (sectionType === 'Episodes' || sectionType === 'Podcasts' || sectionType === 'Profiles') continue
|
if (!goodSections.includes(sectionType)) continue
|
||||||
|
|
||||||
const parsedSectionContents = section.musicShelfRenderer.contents.map((item) => this.parseResponsiveListItemRenderer(item.musicResponsiveListItemRenderer))
|
const parsedSectionContents = section.musicShelfRenderer.contents.map((item) => parseResponsiveListItemRenderer(this.id, item.musicResponsiveListItemRenderer))
|
||||||
parsedSearchResults.push(...parsedSectionContents)
|
parsedSearchResults.push(...parsedSectionContents)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,120 +159,122 @@ export class YouTubeMusic implements Connection {
|
|||||||
const contents = browseResponse.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
|
const contents = browseResponse.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
|
||||||
|
|
||||||
const recommendations: (Song | Album | Artist | Playlist)[] = []
|
const recommendations: (Song | Album | Artist | Playlist)[] = []
|
||||||
|
const goodSections = ['Listen again', 'Forgotten favorites', 'Quick picks', 'From your library']
|
||||||
for (const section of contents) {
|
for (const section of contents) {
|
||||||
const header = section.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text
|
const sectionType = section.musicCarouselShelfRenderer.header.musicCarouselShelfBasicHeaderRenderer.title.runs[0].text
|
||||||
if (header === 'New releases') continue // The 'New Releases Section is generally filled with music that is not tailored to the user'
|
if (!goodSections.includes(sectionType)) continue
|
||||||
|
|
||||||
const parsedContent = section.musicCarouselShelfRenderer.contents.map((content) =>
|
const parsedContent = section.musicCarouselShelfRenderer.contents.map((content) =>
|
||||||
'musicTwoRowItemRenderer' in content ? this.parseTwoRowItemRenderer(content.musicTwoRowItemRenderer) : this.parseResponsiveListItemRenderer(content.musicResponsiveListItemRenderer),
|
'musicTwoRowItemRenderer' in content ? parseTwoRowItemRenderer(this.id, content.musicTwoRowItemRenderer) : parseResponsiveListItemRenderer(this.id, content.musicResponsiveListItemRenderer),
|
||||||
)
|
)
|
||||||
recommendations.push(...parsedContent)
|
recommendations.push(...parsedContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
return recommendations
|
return recommendations
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private parseTwoRowItemRenderer = (rowContent: InnerTube.musicTwoRowItemRenderer): Song | Album | Artist | Playlist => {
|
const parseTwoRowItemRenderer = (connection: string, rowContent: InnerTube.musicTwoRowItemRenderer): Song | Album | Artist | Playlist => {
|
||||||
const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection']
|
const name = rowContent.title.runs[0].text
|
||||||
const name = rowContent.title.runs[0].text
|
|
||||||
|
|
||||||
let artists: (Song | Album)['artists']
|
let artists: (Song | Album)['artists']
|
||||||
for (const run of rowContent.subtitle.runs) {
|
for (const run of rowContent.subtitle.runs) {
|
||||||
if (!run.navigationEndpoint) continue
|
if (!run.navigationEndpoint) continue
|
||||||
|
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||||
|
artists ? artists.push(artist) : (artists = [artist])
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||||
|
const id = rowContent.navigationEndpoint.browseEndpoint.browseId
|
||||||
|
switch (pageType) {
|
||||||
|
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||||
|
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||||
|
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||||
|
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||||
|
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||||
|
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResponsiveListItemRenderer = (connection: string, listContent: InnerTube.musicResponsiveListItemRenderer): Song | Album | Artist | Playlist => {
|
||||||
|
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']
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!('navigationEndpoint' in listContent)) {
|
||||||
|
const id = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId
|
||||||
|
const column2run = listContent.flexColumns[2]?.musicResponsiveListItemFlexColumnRenderer.text.runs?.[0]
|
||||||
|
let album: Song['album']
|
||||||
|
if (column2run?.navigationEndpoint && column2run.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
||||||
|
album = { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = listContent.navigationEndpoint.browseEndpoint.browseId
|
||||||
|
const pageType = listContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||||
|
switch (pageType) {
|
||||||
|
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||||
|
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||||
|
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||||
|
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||||
|
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||||
|
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseMusicCardShelfRenderer = (connection: string, cardContent: InnerTube.musicCardShelfRenderer): Song | Album | Artist | Playlist => {
|
||||||
|
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']
|
||||||
|
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') {
|
||||||
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
const artist = { id: run.navigationEndpoint.browseEndpoint.browseId, name: run.text }
|
||||||
artists ? artists.push(artist) : (artists = [artist])
|
artists ? artists.push(artist) : (artists = [artist])
|
||||||
}
|
} else if (pageType === 'MUSIC_PAGE_TYPE_ALBUM') {
|
||||||
|
album = { 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
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageType = rowContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
|
||||||
const id = rowContent.navigationEndpoint.browseEndpoint.browseId
|
|
||||||
switch (pageType) {
|
|
||||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
|
||||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
|
||||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
|
||||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
|
||||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
|
||||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseResponsiveListItemRenderer = (listContent: InnerTube.musicResponsiveListItemRenderer): Song | Album | Artist | Playlist => {
|
const navigationEndpoint = cardContent.title.runs[0].navigationEndpoint
|
||||||
const connection = { id: this.id, type: 'youtube-music' } satisfies Song['connection']
|
if ('watchEndpoint' in navigationEndpoint) {
|
||||||
const name = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].text
|
const id = navigationEndpoint.watchEndpoint.videoId
|
||||||
const thumbnail = refineThumbnailUrl(listContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
|
||||||
|
|
||||||
let artists: (Song | Album)['artists']
|
|
||||||
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])
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!('navigationEndpoint' in listContent)) {
|
|
||||||
const id = listContent.flexColumns[0].musicResponsiveListItemFlexColumnRenderer.text.runs[0].navigationEndpoint.watchEndpoint.videoId
|
|
||||||
const column2run = listContent.flexColumns[2].musicResponsiveListItemFlexColumnRenderer.text.runs?.[0]
|
|
||||||
const pageIsAlbum = column2run?.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType === 'MUSIC_PAGE_TYPE_ALBUM'
|
|
||||||
const album: Song['album'] = pageIsAlbum ? { id: column2run.navigationEndpoint.browseEndpoint.browseId, name: column2run.text } : undefined
|
|
||||||
|
|
||||||
return { connection, id, name, type: 'song', artists, album, thumbnail } satisfies Song
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageType = listContent.navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
|
||||||
const id = listContent.navigationEndpoint.browseEndpoint.browseId
|
|
||||||
switch (pageType) {
|
|
||||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
|
||||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
|
||||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
|
||||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
|
||||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
|
||||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseMusicCardShelfRenderer = (cardContent: InnerTube.musicCardShelfRenderer): Song | Album | Artist | Playlist => {
|
const pageType = navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
||||||
const connection = { id: this.id, type: 'youtube-music' } satisfies (Song | Album | Artist | Playlist)['connection']
|
const id = navigationEndpoint.browseEndpoint.browseId
|
||||||
const name = cardContent.title.runs[0].text
|
switch (pageType) {
|
||||||
const thumbnail = refineThumbnailUrl(cardContent.thumbnail.musicThumbnailRenderer.thumbnail.thumbnails[0].url)
|
case 'MUSIC_PAGE_TYPE_ALBUM':
|
||||||
|
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
||||||
let album: Song['album'], artists: (Song | Album)['artists']
|
case 'MUSIC_PAGE_TYPE_ARTIST':
|
||||||
for (const run of cardContent.subtitle.runs) {
|
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
||||||
if (!run.navigationEndpoint) continue
|
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
||||||
|
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
||||||
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_ALBUM') {
|
|
||||||
album = { 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
|
|
||||||
}
|
|
||||||
|
|
||||||
const pageType = navigationEndpoint.browseEndpoint.browseEndpointContextSupportedConfigs.browseEndpointContextMusicConfig.pageType
|
|
||||||
const id = navigationEndpoint.browseEndpoint.browseId
|
|
||||||
switch (pageType) {
|
|
||||||
case 'MUSIC_PAGE_TYPE_ALBUM':
|
|
||||||
return { connection, id, name, type: 'album', artists, thumbnail } satisfies Album
|
|
||||||
case 'MUSIC_PAGE_TYPE_ARTIST':
|
|
||||||
return { connection, id, name, type: 'artist', thumbnail } satisfies Artist
|
|
||||||
case 'MUSIC_PAGE_TYPE_PLAYLIST':
|
|
||||||
return { connection, id, name, type: 'playlist', thumbnail } satisfies Playlist
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const refineThumbnailUrl = (urlString: string): string => {
|
const refineThumbnailUrl = (urlString: string): string => {
|
||||||
|
if (!URL.canParse(urlString)) throw new Error('Invalid thumbnail url')
|
||||||
|
|
||||||
const url = new URL(urlString)
|
const url = new URL(urlString)
|
||||||
if (url.origin === 'https://i.ytimg.com') {
|
if (url.origin === 'https://i.ytimg.com') {
|
||||||
return urlString.slice(0, urlString.indexOf('?')).replace('sddefault', 'mqdefault')
|
return urlString.slice(0, urlString.indexOf('?')).replace('sddefault', 'mqdefault')
|
||||||
@@ -452,14 +474,14 @@ declare namespace InnerTube {
|
|||||||
runs?: [
|
runs?: [
|
||||||
{
|
{
|
||||||
text: string
|
text: string
|
||||||
navigationEndpoint: {
|
navigationEndpoint?: {
|
||||||
browseEndpoint: browseEndpoint
|
browseEndpoint: browseEndpoint
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}?,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -4,17 +4,18 @@ import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
|||||||
import type { PageServerLoad, Actions } from './$types'
|
import type { PageServerLoad, Actions } from './$types'
|
||||||
import { DB } from '$lib/server/db'
|
import { DB } from '$lib/server/db'
|
||||||
import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
|
import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
|
||||||
|
import type { ConnectionInfo } from '$lib/server/connections'
|
||||||
import { google } from 'googleapis'
|
import { google } from 'googleapis'
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||||
const connectionsResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
const connectionInfoResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||||
})
|
}).then((response) => response.json())
|
||||||
|
|
||||||
const userConnections = await connectionsResponse.json()
|
const connections: ConnectionInfo[] = connectionInfoResponse.connections
|
||||||
|
|
||||||
return { connections: userConnections.connections }
|
return { connections }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
@@ -28,7 +29,7 @@ export const actions: Actions = {
|
|||||||
|
|
||||||
if (authData instanceof JellyfinFetchError) return fail(authData.httpCode, { message: authData.message })
|
if (authData instanceof JellyfinFetchError) return fail(authData.httpCode, { message: authData.message })
|
||||||
|
|
||||||
const newConnectionId = DB.addConnectionInfo(locals.user.id, { type: 'jellyfin', serviceInfo: { userId: authData.User.Id, urlOrigin: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
|
const newConnectionId = DB.addConnectionInfo({ userId: locals.user.id, type: 'jellyfin', service: { userId: authData.User.Id, urlOrigin: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
|
||||||
|
|
||||||
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
|
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -49,9 +50,10 @@ export const actions: Actions = {
|
|||||||
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['id', 'snippet'], access_token: tokens.access_token! })
|
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['id', 'snippet'], access_token: tokens.access_token! })
|
||||||
const userChannel = userChannelResponse.data.items![0]
|
const userChannel = userChannelResponse.data.items![0]
|
||||||
|
|
||||||
const newConnectionId = DB.addConnectionInfo(locals.user.id, {
|
const newConnectionId = DB.addConnectionInfo({
|
||||||
|
userId: locals.user.id,
|
||||||
type: 'youtube-music',
|
type: 'youtube-music',
|
||||||
serviceInfo: { userId: userChannel.id! },
|
service: { userId: userChannel.id! },
|
||||||
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
|
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||||
|
|
||||||
export let data: PageServerData & LayoutData
|
export let data: PageServerData & LayoutData
|
||||||
let connections: ConnectionInfo[] = data.connections
|
let connections = data.connections
|
||||||
|
|
||||||
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
|
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
|
||||||
const { serverUrl, username, password } = Object.fromEntries(formData)
|
const { serverUrl, username, password } = Object.fromEntries(formData)
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
if (result.type === 'failure') {
|
if (result.type === 'failure') {
|
||||||
return ($newestAlert = ['warning', result.data?.message])
|
return ($newestAlert = ['warning', result.data?.message])
|
||||||
} else if (result.type === 'success') {
|
} else if (result.type === 'success') {
|
||||||
const newConnection: ConnectionInfo = result.data!.newConnection
|
const newConnection = result.data!.newConnection
|
||||||
connections = [...connections, newConnection]
|
connections = [...connections, newConnection]
|
||||||
|
|
||||||
newConnectionModal = null
|
newConnectionModal = null
|
||||||
@@ -72,7 +72,7 @@
|
|||||||
if (result.type === 'failure') {
|
if (result.type === 'failure') {
|
||||||
return ($newestAlert = ['warning', result.data?.message])
|
return ($newestAlert = ['warning', result.data?.message])
|
||||||
} else if (result.type === 'success') {
|
} else if (result.type === 'success') {
|
||||||
const newConnection: ConnectionInfo = result.data!.newConnection
|
const newConnection = result.data!.newConnection
|
||||||
connections = [...connections, newConnection]
|
connections = [...connections, newConnection]
|
||||||
return ($newestAlert = ['success', 'Added Youtube Music'])
|
return ($newestAlert = ['success', 'Added Youtube Music'])
|
||||||
}
|
}
|
||||||
@@ -135,7 +135,14 @@
|
|||||||
</section>
|
</section>
|
||||||
<div id="connection-profile-grid" class="grid gap-8">
|
<div id="connection-profile-grid" class="grid gap-8">
|
||||||
{#each connections as connection}
|
{#each connections as connection}
|
||||||
<ConnectionProfile {connection} submitFunction={profileActions} />
|
<ConnectionProfile
|
||||||
|
id={connection.id}
|
||||||
|
type={connection.type}
|
||||||
|
username={connection.service.username}
|
||||||
|
profilePicture={'profilePicture' in connection.service ? connection.service.profilePicture : undefined}
|
||||||
|
serverName={'serverName' in connection.service ? connection.service.serverName : undefined}
|
||||||
|
submitFunction={profileActions}
|
||||||
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{#if newConnectionModal !== null}
|
{#if newConnectionModal !== null}
|
||||||
|
|||||||
@@ -6,24 +6,22 @@
|
|||||||
import { fly } from 'svelte/transition'
|
import { fly } from 'svelte/transition'
|
||||||
import { enhance } from '$app/forms'
|
import { enhance } from '$app/forms'
|
||||||
|
|
||||||
export let connection: ConnectionInfo
|
export let id: string, type: 'jellyfin' | 'youtube-music', username: string | undefined, profilePicture: string | undefined, serverName: string | undefined
|
||||||
export let submitFunction: SubmitFunction
|
export let submitFunction: SubmitFunction
|
||||||
|
|
||||||
$: serviceData = Services[connection.type]
|
$: serviceData = Services[type]
|
||||||
|
|
||||||
let showModal = false
|
let showModal = false
|
||||||
|
|
||||||
const subHeaderItems: string[] = []
|
const subHeaderItems = [username, serverName]
|
||||||
if ('username' in connection.serviceInfo && connection.serviceInfo.username) subHeaderItems.push(connection.serviceInfo.username)
|
|
||||||
if ('serverName' in connection.serviceInfo && connection.serviceInfo.serverName) subHeaderItems.push(connection.serviceInfo.serverName)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
<section class="rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
||||||
<header class="flex h-20 items-center gap-4 p-4">
|
<header class="flex h-20 items-center gap-4 p-4">
|
||||||
<div class="relative aspect-square h-full p-1">
|
<div class="relative aspect-square h-full p-1">
|
||||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" />
|
<img src={serviceData.icon} alt="{serviceData.displayName} icon" />
|
||||||
{#if 'profilePicture' in connection.serviceInfo && connection.serviceInfo.profilePicture}
|
{#if profilePicture}
|
||||||
<img src={connection.serviceInfo.profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
|
<img src={profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -42,7 +40,7 @@
|
|||||||
<i class="fa-solid fa-link-slash mr-1" />
|
<i class="fa-solid fa-link-slash mr-1" />
|
||||||
Delete Connection
|
Delete Connection
|
||||||
</button>
|
</button>
|
||||||
<input type="hidden" value={connection.id} name="connectionId" />
|
<input type="hidden" value={id} name="connectionId" />
|
||||||
</form>
|
</form>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RequestHandler } from '@sveltejs/kit'
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
import { Connections } from '$lib/server/connections'
|
import { Connections, type ConnectionInfo } from '$lib/server/connections'
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ url }) => {
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
const ids = url.searchParams.get('ids')?.replace(/\s/g, '').split(',')
|
const ids = url.searchParams.get('ids')?.replace(/\s/g, '').split(',')
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const GET: RequestHandler = async ({ url }) => {
|
|||||||
await connection
|
await connection
|
||||||
.search(query)
|
.search(query)
|
||||||
.then((results) => searchResults.push(...results))
|
.then((results) => searchResults.push(...results))
|
||||||
.catch((reason) => console.log(`Failed to search "${query}" from connection ${connection.id}: ${reason}`))
|
.catch((reason) => console.error(`Failed to search "${query}" from connection ${connection.id}: ${reason}`))
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response.json({ searchResults })
|
return Response.json({ searchResults })
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { RequestHandler } from '@sveltejs/kit'
|
import type { RequestHandler } from '@sveltejs/kit'
|
||||||
import { Connections } from '$lib/server/connections'
|
import { Connections, type ConnectionInfo } from '$lib/server/connections'
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const userId = params.userId!
|
const userId = params.userId!
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Connections } from '$lib/server/connections'
|
|||||||
export const GET: RequestHandler = async ({ params }) => {
|
export const GET: RequestHandler = async ({ params }) => {
|
||||||
const userId = params.userId!
|
const userId = params.userId!
|
||||||
|
|
||||||
const recommendations: (Song | Album | Playlist)[] = []
|
const recommendations: (Song | Album | Artist | Playlist)[] = []
|
||||||
for (const connection of Connections.getUserConnections(userId)) {
|
for (const connection of Connections.getUserConnections(userId)) {
|
||||||
await connection
|
await connection
|
||||||
.getRecommendations()
|
.getRecommendations()
|
||||||
|
|||||||
Reference in New Issue
Block a user