ConnectionInfo and the db ConnectionRow types are now completely seperate things. Started on audio fetching yay!
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
<script lang="ts">
|
||||
export let mediaItem: Song | Album | Playlist
|
||||
export let mediaItem: Song | Album | Artist | Playlist
|
||||
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { currentlyPlaying } from '$lib/stores'
|
||||
|
||||
let image: HTMLImageElement, captionText: HTMLDivElement
|
||||
|
||||
const setCurrentlyPlaying = () => {
|
||||
if (mediaItem.type === 'song') {
|
||||
$currentlyPlaying = mediaItem
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id="card-wrapper" class="flex-shrink-0">
|
||||
@@ -17,7 +24,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
<span id="play-button" class="absolute left-1/2 top-1/2 h-12 -translate-x-1/2 -translate-y-1/2 opacity-0 transition-opacity duration-200 ease-out">
|
||||
<IconButton halo={true}>
|
||||
<IconButton halo={true} on:click={setCurrentlyPlaying}>
|
||||
<i slot="icon" class="fa-solid fa-play text-xl" />
|
||||
</IconButton>
|
||||
</span>
|
||||
@@ -39,12 +46,6 @@
|
||||
</div>
|
||||
|
||||
<style>
|
||||
#thumbnail:focus-within #card-image {
|
||||
filter: brightness(50%);
|
||||
}
|
||||
#thumbnail:focus-within #play-button {
|
||||
opacity: 1;
|
||||
}
|
||||
#thumbnail:hover {
|
||||
scale: 1.05;
|
||||
}
|
||||
|
||||
19
src/lib/components/media/mediaPlayer.svelte
Normal file
19
src/lib/components/media/mediaPlayer.svelte
Normal file
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
export let currentlyPlaying: Song
|
||||
</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>
|
||||
|
||||
<style>
|
||||
main {
|
||||
display: grid;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
export let header: string
|
||||
export let cardDataList: (Song | Album | Playlist)[]
|
||||
export let cardDataList: (Song | Album | Artist | Playlist)[]
|
||||
|
||||
import MediaCard from '$lib/components/media/mediaCard.svelte'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import { DB, type DBConnectionInfo } from './db'
|
||||
import { Jellyfin, type JellyfinConnectionInfo } from './jellyfin'
|
||||
import { YouTubeMusic, type YouTubeMusicConnectionInfo } from './youtube-music'
|
||||
import { DB, type ConnectionRow } from './db'
|
||||
import { Jellyfin } from './jellyfin'
|
||||
import { YouTubeMusic } from './youtube-music'
|
||||
|
||||
export type ConnectionInfo = JellyfinConnectionInfo | YouTubeMusicConnectionInfo
|
||||
|
||||
const constructConnection = (connectionInfo: DBConnectionInfo): Connection => {
|
||||
const constructConnection = (connectionInfo: ConnectionRow): Connection => {
|
||||
const { id, userId, type, service, tokens } = connectionInfo
|
||||
switch (type) {
|
||||
case 'jellyfin':
|
||||
return new Jellyfin(id, userId, service.userId, service.urlOrigin, tokens.accessToken)
|
||||
return new Jellyfin(id, userId, service.userId, service.serverUrl, tokens.accessToken)
|
||||
case 'youtube-music':
|
||||
return new YouTubeMusic(id, userId, service.userId, tokens)
|
||||
return new YouTubeMusic(id, userId, service.userId, tokens.accessToken, tokens.refreshToken, tokens.expiry)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,34 +10,32 @@ interface DBConnectionsTableSchema {
|
||||
tokens?: string
|
||||
}
|
||||
|
||||
type JellyfinDBConnection = {
|
||||
export type ConnectionRow = {
|
||||
id: string
|
||||
userId: string
|
||||
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
|
||||
} & (
|
||||
| {
|
||||
type: 'jellyfin'
|
||||
service: {
|
||||
userId: string
|
||||
serverUrl: string
|
||||
}
|
||||
tokens: {
|
||||
accessToken: string
|
||||
}
|
||||
}
|
||||
| {
|
||||
type: 'youtube-music'
|
||||
service: {
|
||||
userId: string
|
||||
}
|
||||
tokens: {
|
||||
accessToken: string
|
||||
refreshToken: string
|
||||
expiry: number
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class Storage {
|
||||
private readonly database: Sqlite3DB
|
||||
@@ -89,8 +87,8 @@ class Storage {
|
||||
if (commandInfo.changes === 0) throw new Error(`User with id ${id} does not exist`)
|
||||
}
|
||||
|
||||
public getConnectionInfo = (ids: string[]): DBConnectionInfo[] => {
|
||||
const connectionInfo: DBConnectionInfo[] = []
|
||||
public getConnectionInfo = (ids: string[]): ConnectionRow[] => {
|
||||
const connectionInfo: ConnectionRow[] = []
|
||||
for (const id of ids) {
|
||||
const result = this.database.prepare(`SELECT * FROM Connections WHERE id = ?`).get(id) as DBConnectionsTableSchema | undefined
|
||||
if (!result) continue
|
||||
@@ -98,23 +96,23 @@ class Storage {
|
||||
const { userId, type, service, tokens } = result
|
||||
const parsedService = service ? JSON.parse(service) : undefined
|
||||
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
||||
connectionInfo.push({ id, userId, type: type as DBConnectionInfo['type'], service: parsedService, tokens: parsedTokens })
|
||||
connectionInfo.push({ id, userId, type: type as ConnectionRow['type'], service: parsedService, tokens: parsedTokens })
|
||||
}
|
||||
return connectionInfo
|
||||
}
|
||||
|
||||
public getUserConnectionInfo = (userId: string): DBConnectionInfo[] => {
|
||||
public getUserConnectionInfo = (userId: string): ConnectionRow[] => {
|
||||
const connectionRows = this.database.prepare(`SELECT * FROM Connections WHERE userId = ?`).all(userId) as DBConnectionsTableSchema[]
|
||||
const connections: DBConnectionInfo[] = []
|
||||
const connections: ConnectionRow[] = []
|
||||
for (const { id, type, service, tokens } of connectionRows) {
|
||||
const parsedService = service ? JSON.parse(service) : undefined
|
||||
const parsedTokens = tokens ? JSON.parse(tokens) : undefined
|
||||
connections.push({ id, userId, type: type as DBConnectionInfo['type'], service: parsedService, tokens: parsedTokens })
|
||||
connections.push({ id, userId, type: type as ConnectionRow['type'], service: parsedService, tokens: parsedTokens })
|
||||
}
|
||||
return connections
|
||||
}
|
||||
|
||||
public addConnectionInfo = (connectionInfo: Omit<DBConnectionInfo, 'id'>): string => {
|
||||
public addConnectionInfo = (connectionInfo: Omit<ConnectionRow, 'id'>): string => {
|
||||
const { userId, type, service, tokens } = connectionInfo
|
||||
const connectionId = generateUUID()
|
||||
this.database.prepare(`INSERT INTO Connections(id, userId, type, service, tokens) VALUES(?, ?, ?, ?, ?)`).run(connectionId, userId, type, JSON.stringify(service), JSON.stringify(tokens))
|
||||
@@ -126,7 +124,7 @@ class Storage {
|
||||
if (commandInfo.changes === 0) throw new Error(`Connection with id: ${id} does not exist`)
|
||||
}
|
||||
|
||||
public updateTokens = (id: string, tokens: DBConnectionInfo['tokens']): void => {
|
||||
public updateTokens = (id: string, tokens: ConnectionRow['tokens']): void => {
|
||||
const commandInfo = this.database.prepare(`UPDATE Connections SET tokens = ? WHERE id = ?`).run(JSON.stringify(tokens), id)
|
||||
if (commandInfo.changes === 0) throw new Error('Failed to update tokens')
|
||||
}
|
||||
|
||||
@@ -1,18 +1,3 @@
|
||||
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 {
|
||||
public id: string
|
||||
private userId: string
|
||||
@@ -41,29 +26,21 @@ export class Jellyfin implements Connection {
|
||||
// userId: this.jfUserId,
|
||||
// })
|
||||
|
||||
public getConnectionInfo = async (): Promise<JellyfinConnectionInfo> => {
|
||||
const userUrl = new URL(`Users/${this.jfUserId}`, this.serverUrl).href
|
||||
const systemUrl = new URL('System/Info', this.serverUrl).href
|
||||
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 userResponse = await fetch(userUrl, { headers: this.BASEHEADERS })
|
||||
const systemResponse = await fetch(systemUrl, { headers: this.BASEHEADERS })
|
||||
|
||||
const userData: JellyfinAPI.User = await userResponse.json()
|
||||
const systemData: JellyfinAPI.System = await systemResponse.json()
|
||||
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())
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
type: 'jellyfin',
|
||||
service: {
|
||||
userId: this.jfUserId,
|
||||
serverUrl: this.serverUrl,
|
||||
username: userData.Name,
|
||||
serverName: systemData.ServerName,
|
||||
},
|
||||
tokens: {
|
||||
accessToken: this.accessToken,
|
||||
},
|
||||
serverUrl: this.serverUrl,
|
||||
serverName: systemData.ServerName,
|
||||
jellyfinUserId: this.jfUserId,
|
||||
username: userData.Name,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +61,10 @@ export class Jellyfin implements Connection {
|
||||
return Array.from(mostPlayedData.Items as JellyfinAPI.Song[], (song) => this.parseSong(song))
|
||||
}
|
||||
|
||||
public getSongAudio = (id: string): string => {
|
||||
return 'need to implement'
|
||||
}
|
||||
|
||||
public search = async (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist'): Promise<(Song | Album | Playlist)[]> => {
|
||||
const searchParams = new URLSearchParams({
|
||||
searchTerm,
|
||||
|
||||
@@ -1,35 +1,24 @@
|
||||
import { google } from 'googleapis'
|
||||
import ytdl from 'ytdl-core'
|
||||
import { DB } from './db'
|
||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||
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 {
|
||||
public id: string
|
||||
private userId: string
|
||||
private ytUserId: string
|
||||
private tokens: YouTubeMusicConnectionInfo['tokens']
|
||||
public readonly id: string
|
||||
private readonly userId: string
|
||||
private readonly ytUserId: string
|
||||
private accessToken: string
|
||||
private readonly refreshToken: string
|
||||
private expiry: number
|
||||
|
||||
constructor(id: string, userId: string, youtubeUserId: string, tokens: YouTubeMusicConnectionInfo['tokens']) {
|
||||
constructor(id: string, userId: string, youtubeUserId: string, accessToken: string, refreshToken: string, expiry: number) {
|
||||
this.id = id
|
||||
this.userId = userId
|
||||
this.ytUserId = youtubeUserId
|
||||
this.tokens = tokens
|
||||
this.accessToken = accessToken
|
||||
this.refreshToken = refreshToken
|
||||
this.expiry = expiry
|
||||
}
|
||||
|
||||
private headers = async () => {
|
||||
@@ -41,21 +30,19 @@ export class YouTubeMusic implements Connection {
|
||||
'content-encoding': 'gzip',
|
||||
origin: 'https://music.youtube.com',
|
||||
Cookie: 'SOCS=CAI;',
|
||||
authorization: `Bearer ${(await this.getTokens()).accessToken}`,
|
||||
authorization: `Bearer ${await this.getAccessToken()}`,
|
||||
'X-Goog-Request-Time': `${Date.now()}`,
|
||||
})
|
||||
}
|
||||
|
||||
private getTokens = async (): Promise<YouTubeMusicConnectionInfo['tokens']> => {
|
||||
if (this.tokens.expiry < Date.now()) {
|
||||
const refreshToken = this.tokens.refreshToken
|
||||
|
||||
private getAccessToken = async (): Promise<string> => {
|
||||
if (this.expiry < Date.now()) {
|
||||
const response = await fetch('https://oauth2.googleapis.com/token', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
client_id: PUBLIC_YOUTUBE_API_CLIENT_ID,
|
||||
client_secret: YOUTUBE_API_CLIENT_SECRET,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token: this.refreshToken,
|
||||
grant_type: 'refresh_token',
|
||||
}),
|
||||
})
|
||||
@@ -63,29 +50,26 @@ export class YouTubeMusic implements Connection {
|
||||
const { access_token, expires_in } = await response.json()
|
||||
const newExpiry = Date.now() + expires_in * 1000
|
||||
|
||||
const newTokens: YouTubeMusicConnectionInfo['tokens'] = { accessToken: access_token, refreshToken, expiry: newExpiry }
|
||||
DB.updateTokens(this.id, newTokens)
|
||||
this.tokens = newTokens
|
||||
DB.updateTokens(this.id, { accessToken: access_token, refreshToken: this.refreshToken, expiry: newExpiry })
|
||||
this.accessToken = access_token
|
||||
this.expiry = newExpiry
|
||||
}
|
||||
|
||||
return this.tokens
|
||||
return this.accessToken
|
||||
}
|
||||
|
||||
public getConnectionInfo = async (): Promise<YouTubeMusicConnectionInfo> => {
|
||||
public getConnectionInfo = async (): Promise<Extract<ConnectionInfo, { type: 'youtube-music' }>> => {
|
||||
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.getAccessToken() })
|
||||
const userChannel = userChannelResponse.data.items![0]
|
||||
|
||||
return {
|
||||
id: this.id,
|
||||
userId: this.userId,
|
||||
type: 'youtube-music',
|
||||
service: {
|
||||
userId: this.ytUserId,
|
||||
username: userChannel.snippet?.title as string,
|
||||
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string,
|
||||
},
|
||||
tokens: await this.getTokens(),
|
||||
youtubeUserId: this.ytUserId,
|
||||
username: userChannel.snippet?.title!,
|
||||
profilePicture: userChannel.snippet?.thumbnails?.default?.url!,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,7 +99,6 @@ export class YouTubeMusic implements Connection {
|
||||
},
|
||||
}),
|
||||
}).then((response) => response.json())
|
||||
console.log(JSON.stringify(searchResulsts))
|
||||
|
||||
const contents = searchResulsts.contents.tabbedSearchResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents
|
||||
|
||||
@@ -172,6 +155,13 @@ export class YouTubeMusic implements Connection {
|
||||
|
||||
return recommendations
|
||||
}
|
||||
|
||||
public getSongAudio = async (id: string): Promise<ReadableStream<Uint8Array>> => {
|
||||
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!
|
||||
}
|
||||
}
|
||||
|
||||
const parseTwoRowItemRenderer = (connection: string, rowContent: InnerTube.musicTwoRowItemRenderer): Song | Album | Artist | Playlist => {
|
||||
|
||||
@@ -5,7 +5,7 @@ export const pageWidth: Writable<number> = writable()
|
||||
|
||||
export const newestAlert: Writable<[AlertType, string]> = writable()
|
||||
|
||||
export const currentlyPlaying = writable()
|
||||
export const currentlyPlaying: Writable<Song | null> = writable()
|
||||
|
||||
const youtubeMusicBackground: string = 'https://www.gstatic.com/youtube/media/ytm/images/sbg/wsbg@4000x2250.png' // Default Youtube music background
|
||||
export const backgroundImage: Writable<string> = writable(youtubeMusicBackground)
|
||||
|
||||
Reference in New Issue
Block a user