Lazuli, now with openapi!

This commit is contained in:
Eclypsed
2024-02-18 00:01:54 -05:00
parent 85a17dcd89
commit 1608e03b97
14 changed files with 177 additions and 291 deletions

Binary file not shown.

View File

@@ -5,7 +5,15 @@ import { isValidURL } from '$lib/utils'
const db = new Database('./src/lib/server/users.db', { verbose: console.info })
db.pragma('foreign_keys = ON')
const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id VARCHAR(36) PRIMARY KEY, username VARCHAR(30) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)'
const initConnectionsTable = 'CREATE TABLE IF NOT EXISTS Connections(id VARCHAR(36) PRIMARY KEY, userId VARCHAR(36) NOT NULL, service TEXT NOT NULL, tokens TEXT NOT NULL, FOREIGN KEY(userId) REFERENCES Users(id))'
const initConnectionsTable = `CREATE TABLE IF NOT EXISTS Connections(
id VARCHAR(36) PRIMARY KEY,
userId VARCHAR(36) NOT NULL,
service TEXT NOT NULL,
accessToken TEXT NOT NULL,
refreshToken TEXT,
expiry NUMBER,
FOREIGN KEY(userId) REFERENCES Users(id)
)`
db.exec(initUsersTable), db.exec(initConnectionsTable)
type UserQueryParams = {
@@ -16,7 +24,9 @@ interface ConnectionsTableSchema {
id: string
userId: string
service: string
tokens: string
accessToken: string
refreshToken?: string
expiry?: number
}
export class Users {
@@ -52,8 +62,8 @@ export class Users {
export class Connections {
static getConnection = (id: string): Connection => {
const { userId, service, tokens } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as ConnectionsTableSchema
const connection: Connection = { id, userId, service: JSON.parse(service), tokens: JSON.parse(tokens) }
const { userId, service, accessToken, refreshToken, expiry } = db.prepare('SELECT * FROM Connections WHERE id = ?').get(id) as ConnectionsTableSchema
const connection: Connection = { id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry }
return connection
}
@@ -61,16 +71,16 @@ export class Connections {
const connectionRows = db.prepare('SELECT * FROM Connections WHERE userId = ?').all(userId) as ConnectionsTableSchema[]
const connections: Connection[] = []
for (const row of connectionRows) {
const { id, service, tokens } = row
connections.push({ id, userId, service: JSON.parse(service), tokens: JSON.parse(tokens) })
const { id, service, accessToken, refreshToken, expiry } = row
connections.push({ id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry })
}
return connections
}
static addConnection = (userId: string, service: Service, tokens: Tokens): Connection => {
static addConnection = (userId: string, service: Service, accessToken: string, refreshToken?: string, expiry?: number): Connection => {
const connectionId = generateUUID()
if (!isValidURL(service.urlOrigin)) throw new Error('Service does not have valid url')
db.prepare('INSERT INTO Connections(id, userId, service, tokens) VALUES(?, ?, ?, ?)').run(connectionId, userId, JSON.stringify(service), JSON.stringify(tokens))
db.prepare('INSERT INTO Connections(id, userId, service, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?)').run(connectionId, userId, JSON.stringify(service), accessToken, refreshToken, expiry)
return this.getConnection(connectionId)
}
@@ -79,8 +89,18 @@ export class Connections {
if (commandInfo.changes === 0) throw new Error(`Connection with id: ${id} does not exist`)
}
static updateTokens = (id: string, tokens: Tokens): void => {
const commandInfo = db.prepare('UPDATE Connections SET tokens = ? WHERE id = ?').run(JSON.stringify(tokens), id)
static updateTokens = (id: string, accessToken: string, refreshToken?: string, expiry?: number): void => {
const commandInfo = db.prepare('UPDATE Connections SET accessToken = ?, refreshToken = ?, expiry = ? WHERE id = ?').run(accessToken, refreshToken, expiry, id)
if (commandInfo.changes === 0) throw new Error('Failed to update tokens')
}
static getExpiredConnections = (userId: string): Connection[] => {
const expiredRows = db.prepare('SELECT * FROM Connections WHERE userId = ? AND expiry < ?').all(userId, Date.now()) as ConnectionsTableSchema[]
const connections: Connection[] = []
for (const row of expiredRows) {
const { id, userId, service, accessToken, refreshToken, expiry } = row
connections.push({ id, userId, service: JSON.parse(service), accessToken, refreshToken, expiry })
}
return connections
}
}

View File

@@ -10,7 +10,7 @@ export class Jellyfin {
}
}
static songFactory = (song: Jellyfin.Song, connection: Jellyfin.JFConnection): Song => {
static songFactory = (song: Jellyfin.Song, connection: Connection): Song => {
const { id, service } = connection
const thumbnail = song.ImageTags?.Primary
? new URL(`Items/${song.Id}/Images/Primary`, service.urlOrigin).href
@@ -42,7 +42,7 @@ export class Jellyfin {
}
}
static albumFactory = (album: Jellyfin.Album, connection: Jellyfin.JFConnection): Album => {
static albumFactory = (album: Jellyfin.Album, connection: Connection): Album => {
const { id, service } = connection
const thumbnail = album.ImageTags?.Primary ? new URL(`Items/${album.Id}/Images/Primary`, service.urlOrigin).href : undefined
@@ -72,7 +72,7 @@ export class Jellyfin {
}
}
static playListFactory = (playlist: Jellyfin.Playlist, connection: Jellyfin.JFConnection): Playlist => {
static playListFactory = (playlist: Jellyfin.Playlist, connection: Connection): Playlist => {
const { id, service } = connection
const thumbnail = playlist.ImageTags?.Primary ? new URL(`Items/${playlist.Id}/Images/Primary`, service.urlOrigin).href : undefined
@@ -87,7 +87,7 @@ export class Jellyfin {
}
}
static artistFactory = (artist: Jellyfin.Artist, connection: Jellyfin.JFConnection): Artist => {
static artistFactory = (artist: Jellyfin.Artist, connection: Connection): Artist => {
const { id, service } = connection
const thumbnail = artist.ImageTags?.Primary ? new URL(`Items/${artist.Id}/Images/Primary`, service.urlOrigin).href : undefined
@@ -100,24 +100,4 @@ export class Jellyfin {
thumbnail,
}
}
static connectionInfo = async (connection: Jellyfin.JFConnection): Promise<ConnectionInfo> => {
const reqHeaders = new Headers({ Authorization: `MediaBrowser Token="${connection.tokens.accessToken}"` })
const userUrl = new URL(`Users/${connection.service.userId}`, connection.service.urlOrigin).href
const systemUrl = new URL('System/Info', connection.service.urlOrigin).href
const userResponse = await fetch(userUrl, { headers: reqHeaders })
const systemResponse = await fetch(systemUrl, { headers: reqHeaders })
const userData: Jellyfin.User = await userResponse.json()
const systemData: Jellyfin.System = await systemResponse.json()
return {
connectionId: connection.id,
serviceType: 'jellyfin',
username: userData.Name,
serverName: systemData.ServerName,
}
}
}

View File

@@ -1,46 +1,3 @@
import { YOUTUBE_API_CLIENT_SECRET } from '$env/static/private'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
import { Connections } from '$lib/server/users'
import { google } from 'googleapis'
export class YouTubeMusic {
static refreshAccessToken = async (connectionId: string, refreshToken: string): Promise<Tokens> => {
// Again DON'T SHIP THIS, CLIENT SECRET SHOULD NOT BE EXPOSED TO USERS
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,
grant_type: 'refresh_token',
}),
})
const { access_token, expires_in } = await response.json()
const newTokens: Tokens = {
accessToken: access_token,
refreshToken,
expiry: Date.now() + expires_in * 1000,
}
Connections.updateTokens(connectionId, newTokens)
return newTokens
}
static connectionInfo = async (connection: YouTubeMusic.YTConnection): Promise<ConnectionInfo> => {
let accessToken = connection.tokens.accessToken
if (Date.now() > connection.tokens.expiry) {
const newTokenData = await this.refreshAccessToken(connection.id, connection.tokens.refreshToken)
accessToken = newTokenData.accessToken
}
const youtube = google.youtube('v3')
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['snippet'], access_token: accessToken })
const userChannel = userChannelResponse.data.items![0]
return {
connectionId: connection.id,
serviceType: connection.service.type,
username: userChannel.snippet?.title as string,
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined,
}
}
}
export class YouTubeMusic {}