ConnectionInfo and the db ConnectionRow types are now completely seperate things. Started on audio fetching yay!

This commit is contained in:
Eclypsed
2024-04-05 02:00:17 -04:00
parent 952c8383f9
commit c5408d76b6
20 changed files with 220 additions and 253 deletions

View File

@@ -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;
}

View 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>

View File

@@ -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'

View File

@@ -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)
}
}

View File

@@ -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')
}

View File

@@ -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,

View File

@@ -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 => {

View File

@@ -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)