mirror of
https://codeberg.org/lucida/lucida.git
synced 2026-11-12 06:37:49 -05:00
add spotify support
This commit is contained in:
@@ -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
|
||||
|
||||
84
src/streamers/spotify/main.ts
Normal file
84
src/streamers/spotify/main.ts
Normal 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
|
||||
57
src/streamers/spotify/parse.ts
Normal file
57
src/streamers/spotify/parse.ts
Normal 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))
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user