add spotify support

This commit is contained in:
hazycora
2023-09-16 23:39:32 -04:00
parent 8af3b9f926
commit c321cfab05
7 changed files with 294 additions and 2 deletions

View File

@@ -25,7 +25,7 @@ class Lucida {
if (!this.logins) throw new Error('No logins specified')
for (const i in this.logins) {
const credentials = this.logins[i]
await this.modules[i].login?.(credentials.username, credentials.password)
await this.modules[i]?.login?.(credentials.username, credentials.password)
}
}
async search(query: string, limit: number): Promise<{ [key: string]: SearchResults }> {
@@ -53,6 +53,13 @@ class Lucida {
}
throw new Error(`Couldn't find module for hostname ${urlObj.hostname}`)
}
disconnect() {
return Promise.all(
Object.values(this.modules).map((e) => {
return e.disconnect?.()
})
)
}
}
export default Lucida

View File

@@ -0,0 +1,84 @@
import Librespot from 'librespot'
import { parseArtist, parseAlbum, parseTrack } from './parse.js'
import { GetByUrlResponse, SearchResults, StreamerWithLogin } from '../../types.js'
import { SpotifyAlbum, SpotifyArtist } from 'librespot/build/utils/types'
class Spotify implements StreamerWithLogin {
client: Librespot
hostnames = ['open.spotify.com']
constructor(options: never) {
this.client = new Librespot(options)
}
login(username: string, password: string) {
return this.client.login(username, password)
}
#getUrlParts(url: string): ['artist' | 'album' | 'track', string] {
const urlObj = new URL(url)
const parts = urlObj.pathname.slice(1).split('/')
if (parts.length > 2) throw new Error('Unknown Spotify URL')
if (parts[0] != 'artist' && parts[0] != 'track' && parts[0] != 'album') {
throw new Error(`Spotify type "${parts[0]}" unsupported`)
}
if (!parts[1]) throw new Error('Unknown Spotify URL')
return [parts[0], parts[1]]
}
getTypeFromUrl(url: string) {
return this.#getUrlParts(url)[0]
}
async getByUrl(url: string): Promise<GetByUrlResponse> {
const [type, id] = this.#getUrlParts(url)
if (type == 'track') {
const metadata = await this.client.get.trackMetadata(id)
return {
type,
getStream: async () => {
const streamData = await this.client.get.trackStream(id)
return {
mimeType: 'audio/ogg',
sizeBytes: streamData.sizeBytes,
stream: streamData.stream
}
},
metadata: parseTrack(metadata)
}
}
const data = await this.client.get.byUrl(url)
let metadata
switch (type) {
case 'artist':
metadata = parseArtist(<SpotifyArtist>data)
return {
type,
metadata
}
case 'album':
metadata = parseAlbum(<SpotifyAlbum>data)
if ((<SpotifyAlbum>data).tracks) {
return {
type,
metadata,
tracks: (<SpotifyAlbum>data).tracks?.map((e) => parseTrack(e)) ?? []
}
}
return {
type,
metadata,
tracks: []
}
}
}
async search(query: string): Promise<SearchResults> {
const results = await this.client.browse.search(query)
return {
query,
albums: results.albums?.map((e) => parseAlbum(e)) ?? [],
artists: results.artists?.map((e) => parseArtist(e)) ?? [],
tracks: results.tracks?.map((e) => parseTrack(e)) ?? []
}
}
disconnect() {
return this.client.disconnect()
}
}
export default Spotify

View File

@@ -0,0 +1,57 @@
import type {
SpotifyAlbum,
SpotifyArtist,
SpotifyThumbnail,
SpotifyTrack
} from 'librespot/build/utils/types'
import { Album, Artist, Track } from '../../types'
function parseThumbnails(raw: SpotifyThumbnail[]) {
return raw
.sort((a, b) => (a.width ?? 0) - (b.width ?? 0))
.map((e) => {
return {
width: e.width ?? 0,
height: e.height ?? 0,
url: e.url
}
})
}
export function parseArtist(raw: SpotifyArtist) {
const artist: Artist = {
id: raw.id,
url: raw.externalUrl,
name: raw.name
}
if (raw.avatar) artist.pictures = parseThumbnails(raw.avatar).map((e) => e.url)
if (raw.albums) artist.albums = raw.albums.map((e) => parseAlbum(e))
return artist
}
export function parseTrack(raw: SpotifyTrack) {
const track: Track = {
title: raw.name,
id: raw.id,
url: raw.externalUrl,
explicit: raw.explicit,
trackNumber: raw.trackNumber,
discNumber: raw.discNumber,
artists: raw.artists?.map((e) => parseArtist(e)) ?? [],
durationMs: raw.durationMs
}
if (raw.album) track.album = parseAlbum(raw.album)
return track
}
export function parseAlbum(raw: SpotifyAlbum): Album {
return {
title: raw.name,
id: raw.id,
url: raw.externalUrl,
trackCount: raw.totalTracks,
releaseDate: raw.releaseDate,
coverArtwork: parseThumbnails(raw.coverArtwork),
artists: raw.artists.map((e) => parseArtist(e))
}
}

View File

@@ -86,6 +86,7 @@ export interface Streamer {
search(query: string, limit: number): Promise<SearchResults>
getTypeFromUrl(url: string): ItemType
getByUrl(url: string): Promise<GetByUrlResponse>
disconnect?(): Promise<void>
}
export interface StreamerWithLogin extends Streamer {