mirror of
https://git.gay/lucida/lucida
synced 2025-12-11 20:15:14 +01:00
format and lint
lmfao why has nobody been doing this
This commit is contained in:
parent
62e032fea8
commit
233adf68f7
13
src/index.ts
13
src/index.ts
@ -1,4 +1,11 @@
|
||||
import { ItemType, GetByUrlResponse, SearchResults, Streamer, StreamerWithLogin, StreamerAccount } from './types.js'
|
||||
import {
|
||||
ItemType,
|
||||
GetByUrlResponse,
|
||||
SearchResults,
|
||||
Streamer,
|
||||
StreamerWithLogin,
|
||||
StreamerAccount
|
||||
} from './types.js'
|
||||
|
||||
interface LucidaOptions {
|
||||
modules: { [key: string]: Streamer | StreamerWithLogin }
|
||||
@ -41,8 +48,8 @@ class Lucida {
|
||||
async checkAccounts(): Promise<{ [key: string]: StreamerAccount }> {
|
||||
const results = await Promise.all(
|
||||
Object.values(this.modules).map(async (e) => {
|
||||
if (e.getAccountInfo) return (await e.getAccountInfo())
|
||||
else return {valid: false}
|
||||
if (e.getAccountInfo) return await e.getAccountInfo()
|
||||
else return { valid: false }
|
||||
})
|
||||
)
|
||||
const moduleNames = Object.keys(this.modules)
|
||||
|
||||
@ -33,7 +33,7 @@ interface LoginResponse {
|
||||
id: number
|
||||
display_name: string
|
||||
language_code: string
|
||||
zone: string,
|
||||
zone: string
|
||||
store: string
|
||||
country: string
|
||||
creation_date: string
|
||||
@ -272,8 +272,12 @@ export default class Qobuz implements StreamerWithLogin {
|
||||
}
|
||||
}
|
||||
async getAccountInfo(): Promise<StreamerAccount> {
|
||||
const loginResponse = <LoginResponse>await this.#getSigned('user/login', {extra: 'partner', device_manufacturer_id: 'undefined', app_id: this.appId})
|
||||
|
||||
const loginResponse = <LoginResponse>await this.#getSigned('user/login', {
|
||||
extra: 'partner',
|
||||
device_manufacturer_id: 'undefined',
|
||||
app_id: this.appId
|
||||
})
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
premium: loginResponse.user.credential.parameters.hires_streaming,
|
||||
|
||||
@ -49,12 +49,12 @@ export interface RawAlbum {
|
||||
upc: string
|
||||
released_at: number
|
||||
label?: {
|
||||
name: string,
|
||||
name: string
|
||||
id: number
|
||||
},
|
||||
}
|
||||
genre?: {
|
||||
name: string,
|
||||
id: number,
|
||||
name: string
|
||||
id: number
|
||||
slug: string
|
||||
}
|
||||
copyright: string
|
||||
@ -102,9 +102,9 @@ export interface RawTrack {
|
||||
album?: RawAlbum
|
||||
track_number?: number
|
||||
media_number?: number
|
||||
duration: number,
|
||||
duration: number
|
||||
parental_warning: boolean
|
||||
isrc: string,
|
||||
isrc: string
|
||||
performers?: string
|
||||
}
|
||||
|
||||
@ -147,4 +147,4 @@ function parsePerformers(performers: string, track: Track) {
|
||||
}
|
||||
|
||||
return track
|
||||
}
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ export default class Soundcloud implements Streamer {
|
||||
else if (resultResponse.collection[i].kind == 'playlist')
|
||||
items.albums.push(await parseAlbum(<RawAlbum>resultResponse.collection[i]))
|
||||
else if (resultResponse.collection[i].kind == 'user')
|
||||
items.artists.push((await parseArtist(<RawArtist>resultResponse.collection[i])))
|
||||
items.artists.push(await parseArtist(<RawArtist>resultResponse.collection[i]))
|
||||
}
|
||||
|
||||
return items
|
||||
@ -143,21 +143,18 @@ export default class Soundcloud implements Streamer {
|
||||
|
||||
switch (type) {
|
||||
case 'track': {
|
||||
const trackId = html
|
||||
.split(`"soundcloud://sounds:`)?.[1]
|
||||
?.split(`">`)?.[0]
|
||||
|
||||
const trackId = html.split(`"soundcloud://sounds:`)?.[1]?.split(`">`)?.[0]
|
||||
|
||||
let naked = `https://api-v2.soundcloud.com/tracks/${trackId}`
|
||||
let path = new URL(url).pathname
|
||||
const path = new URL(url).pathname
|
||||
if (path.split('/').length == 4) naked = `${naked}?secret_token=${path.split('/')[3]}`
|
||||
|
||||
const api = JSON.parse(
|
||||
await (
|
||||
await fetch(
|
||||
this.#formatURL(naked, client),
|
||||
{ method: 'get', headers: headers(this.oauthToken) }
|
||||
)
|
||||
await fetch(this.#formatURL(naked, client), {
|
||||
method: 'get',
|
||||
headers: headers(this.oauthToken)
|
||||
})
|
||||
).text()
|
||||
)
|
||||
|
||||
@ -215,11 +212,11 @@ export default class Soundcloud implements Streamer {
|
||||
|
||||
return {
|
||||
type: 'artist',
|
||||
metadata: (await parseArtist(data))
|
||||
metadata: await parseArtist(data)
|
||||
}
|
||||
}
|
||||
default:
|
||||
throw(`Type "${type}" not supported.`)
|
||||
throw `Type "${type}" not supported.`
|
||||
}
|
||||
}
|
||||
async #getRawTrackInfo(id: number | string, client: ScClient) {
|
||||
@ -232,16 +229,17 @@ export default class Soundcloud implements Streamer {
|
||||
).text()
|
||||
)
|
||||
|
||||
return {...api, id}
|
||||
return { ...api, id }
|
||||
}
|
||||
async getAccountInfo(): Promise<StreamerAccount> {
|
||||
const track = <TrackGetByUrlResponse>await this.getByUrl('https://soundcloud.com/ween/polka-dot-tail')
|
||||
const track = <TrackGetByUrlResponse>(
|
||||
await this.getByUrl('https://soundcloud.com/ween/polka-dot-tail')
|
||||
)
|
||||
const stream = await track.getStream()
|
||||
if (stream.mimeType.startsWith('audio/mp4')) {
|
||||
stream.stream.unpipe()
|
||||
return {valid: true, premium: true, explicit: true}
|
||||
}
|
||||
else return {valid: true, premium: false, explicit: true}
|
||||
return { valid: true, premium: true, explicit: true }
|
||||
} else return { valid: true, premium: false, explicit: true }
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,11 +275,11 @@ async function getStream(
|
||||
): Promise<GetStreamResponse> {
|
||||
let filter = transcodings.filter((x) => x.quality == 'hq')
|
||||
if (hq == true && filter.length == 0) throw new Error('Could not find HQ format.')
|
||||
|
||||
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('aac_')) // prioritize aac (go+)
|
||||
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('mp3_')) // then mp3
|
||||
|
||||
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('aac_')) // prioritize aac (go+)
|
||||
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('mp3_')) // then mp3
|
||||
if (filter.length == 0) filter = transcodings.filter((x) => x.preset.startsWith('opus_')) // then opus
|
||||
if (filter.length == 0) throw new Error('Could not find applicable format.') // and this is just in case none of those exist
|
||||
if (filter.length == 0) throw new Error('Could not find applicable format.') // and this is just in case none of those exist
|
||||
|
||||
const transcoding = filter[0]
|
||||
const streamUrlResp = await fetch(
|
||||
@ -305,7 +303,7 @@ async function getStream(
|
||||
|
||||
return {
|
||||
mimeType: transcoding.format.mime_type,
|
||||
stream: (await parseHls(json.url, container))
|
||||
stream: await parseHls(json.url, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ export async function parseAlbum(raw: RawAlbum): Promise<Album> {
|
||||
url: raw.permalink_url,
|
||||
trackCount: raw.track_count,
|
||||
releaseDate: new Date(raw.release_date),
|
||||
artists: [(await parseArtist(raw.user))]
|
||||
artists: [await parseArtist(raw.user)]
|
||||
}
|
||||
if (raw.tracks?.[0]?.artwork_url != undefined) {
|
||||
album.coverArtwork = [await parseCoverArtwork(raw?.tracks?.[0]?.artwork_url)]
|
||||
@ -82,13 +82,13 @@ export interface RawTrack {
|
||||
kind: 'track'
|
||||
id: number | string
|
||||
title: string
|
||||
duration: number,
|
||||
created_at: string,
|
||||
full_duration: number,
|
||||
duration: number
|
||||
created_at: string
|
||||
full_duration: number
|
||||
permalink_url: string
|
||||
artwork_url?: string,
|
||||
user: RawArtist,
|
||||
last_modified: string,
|
||||
artwork_url?: string
|
||||
user: RawArtist
|
||||
last_modified: string
|
||||
description: string
|
||||
user_id: number | string
|
||||
}
|
||||
@ -98,21 +98,22 @@ export async function parseTrack(raw: RawTrack): Promise<Track> {
|
||||
id: raw.id,
|
||||
title: raw.title,
|
||||
url: raw.permalink_url,
|
||||
artists: [(await parseArtist(raw.user))],
|
||||
durationMs: (raw.full_duration || raw.media?.transcodings?.[0]?.duration),
|
||||
artists: [await parseArtist(raw.user)],
|
||||
durationMs: raw.full_duration || raw.media?.transcodings?.[0]?.duration,
|
||||
releaseDate: new Date(raw.created_at),
|
||||
description: raw.description
|
||||
}
|
||||
|
||||
if (raw?.artwork_url != undefined) track.coverArtwork = [await parseCoverArtwork(raw?.artwork_url)]
|
||||
if (raw?.artwork_url != undefined)
|
||||
track.coverArtwork = [await parseCoverArtwork(raw?.artwork_url)]
|
||||
|
||||
return track
|
||||
}
|
||||
|
||||
export async function parseHls(url: string, container: string): Promise<NodeJS.ReadableStream> {
|
||||
return new Promise(async function(resolve, reject) {
|
||||
const folder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'lucida'))
|
||||
const folder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'lucida'))
|
||||
|
||||
return new Promise(function (resolve, reject) {
|
||||
const ffmpegProc = spawn('ffmpeg', [
|
||||
'-hide_banner',
|
||||
'-loglevel',
|
||||
@ -128,13 +129,13 @@ export async function parseHls(url: string, container: string): Promise<NodeJS.R
|
||||
|
||||
let err: string
|
||||
|
||||
ffmpegProc.stderr.on('data', function(data) {
|
||||
ffmpegProc.stderr.on('data', function (data) {
|
||||
err = data.toString()
|
||||
})
|
||||
|
||||
ffmpegProc.once('exit', function(code) {
|
||||
|
||||
ffmpegProc.once('exit', function (code) {
|
||||
if (code == 0) resolve(fs.createReadStream(`${folder}/hls.${container}`))
|
||||
else reject((`FFMPEG HLS error: ${err}` || 'FFMPEG could not parse the HLS.'))
|
||||
else reject(`FFMPEG HLS error: ${err}` || 'FFMPEG could not parse the HLS.')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import Librespot, { LibrespotOptions } from 'librespot'
|
||||
import { parseArtist, parseAlbum, parseTrack, parseEpisode, parsePodcast } from './parse.js'
|
||||
import { GetByUrlResponse, ItemType, SearchResults, StreamerAccount, StreamerWithLogin } from '../../types.js'
|
||||
import {
|
||||
GetByUrlResponse,
|
||||
ItemType,
|
||||
SearchResults,
|
||||
StreamerAccount,
|
||||
StreamerWithLogin
|
||||
} from '../../types.js'
|
||||
|
||||
class Spotify implements StreamerWithLogin {
|
||||
client: Librespot
|
||||
@ -15,7 +21,13 @@ class Spotify implements StreamerWithLogin {
|
||||
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' && parts[0] != 'show' && parts[0] != 'episode') {
|
||||
if (
|
||||
parts[0] != 'artist' &&
|
||||
parts[0] != 'track' &&
|
||||
parts[0] != 'album' &&
|
||||
parts[0] != 'show' &&
|
||||
parts[0] != 'episode'
|
||||
) {
|
||||
throw new Error(`Spotify type "${parts[0]}" unsupported`)
|
||||
}
|
||||
if (!parts[1]) throw new Error('Unknown Spotify URL')
|
||||
@ -63,7 +75,7 @@ class Spotify implements StreamerWithLogin {
|
||||
if (tracks) {
|
||||
return {
|
||||
type,
|
||||
metadata: {...parseAlbum(metadata), trackCount: tracks.length},
|
||||
metadata: { ...parseAlbum(metadata), trackCount: tracks.length },
|
||||
tracks: tracks?.map((e) => parseTrack(e)) ?? []
|
||||
}
|
||||
}
|
||||
@ -93,7 +105,7 @@ class Spotify implements StreamerWithLogin {
|
||||
return {
|
||||
type: 'podcast',
|
||||
metadata: parsePodcast(metadata),
|
||||
episodes: (metadata.episodes?.map((e) => parseEpisode(e))) ?? []
|
||||
episodes: metadata.episodes?.map((e) => parseEpisode(e)) ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,7 +128,7 @@ class Spotify implements StreamerWithLogin {
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
premium,
|
||||
premium,
|
||||
country: info.country,
|
||||
explicit: info.allowExplicit
|
||||
}
|
||||
|
||||
@ -1,4 +1,11 @@
|
||||
import type { SpotifyAlbum, SpotifyArtist, SpotifyThumbnail, SpotifyTrack, SpotifyEpisode, SpotifyPodcast } from 'librespot/types'
|
||||
import type {
|
||||
SpotifyAlbum,
|
||||
SpotifyArtist,
|
||||
SpotifyThumbnail,
|
||||
SpotifyTrack,
|
||||
SpotifyEpisode,
|
||||
SpotifyPodcast
|
||||
} from 'librespot/types'
|
||||
import { Album, Artist, Episode, Podcast, Track } from '../../types.js'
|
||||
|
||||
function parseThumbnails(raw: SpotifyThumbnail[]) {
|
||||
@ -33,7 +40,7 @@ export function parseTrack(raw: SpotifyTrack) {
|
||||
trackNumber: raw.trackNumber,
|
||||
discNumber: raw.discNumber,
|
||||
artists: raw.artists?.map((e) => parseArtist(e)) ?? [],
|
||||
durationMs: raw.durationMs,
|
||||
durationMs: raw.durationMs
|
||||
}
|
||||
if (raw.album) track.album = parseAlbum(raw.album)
|
||||
if (raw?.isrc) track.isrc = raw.isrc
|
||||
@ -49,7 +56,7 @@ export function parseAlbum(raw: SpotifyAlbum) {
|
||||
trackCount: raw.totalTracks,
|
||||
releaseDate: raw.releaseDate,
|
||||
coverArtwork: parseThumbnails(raw.coverArtwork),
|
||||
artists: raw.artists.map((e) => parseArtist(e)),
|
||||
artists: raw.artists.map((e) => parseArtist(e))
|
||||
}
|
||||
|
||||
if (raw.availableMarkets) album.regions = raw.availableMarkets
|
||||
@ -80,8 +87,8 @@ export function parsePodcast(raw: SpotifyPodcast) {
|
||||
description: raw.description,
|
||||
coverArtwork: parseThumbnails(raw.coverArtwork)
|
||||
}
|
||||
if (typeof raw.explicit == 'boolean') podcast.explicit = raw.explicit
|
||||
if (typeof raw.explicit == 'boolean') podcast.explicit = raw.explicit
|
||||
if (raw.episodes) podcast.episodes = raw.episodes.map((e) => parseEpisode(e))
|
||||
|
||||
return podcast
|
||||
}
|
||||
}
|
||||
|
||||
@ -55,7 +55,7 @@ interface SubscriptionData {
|
||||
highestSoundQuality: string
|
||||
premiumAccess: boolean
|
||||
canGetTrial: boolean
|
||||
paymentType: string,
|
||||
paymentType: string
|
||||
paymentOverdue: boolean
|
||||
}
|
||||
|
||||
@ -95,7 +95,11 @@ export default class Tidal implements Streamer {
|
||||
'User-Agent': 'TIDAL_ANDROID/1039 okhttp/3.14.9'
|
||||
}
|
||||
}
|
||||
async #get(url: string, params: { [key: string]: string | number } = {}, base: string = TIDAL_API_BASE): Promise<unknown> {
|
||||
async #get(
|
||||
url: string,
|
||||
params: { [key: string]: string | number } = {},
|
||||
base: string = TIDAL_API_BASE
|
||||
): Promise<unknown> {
|
||||
if (this.failedAuth) throw new Error(`Last request failed to authorize, get new tokens`)
|
||||
if (Date.now() > this.expires) await this.refresh()
|
||||
if (!this.countryCode) await this.getCountryCode()
|
||||
@ -300,7 +304,10 @@ export default class Tidal implements Streamer {
|
||||
tracks: tracksResponse.items.map(parseTrack)
|
||||
}
|
||||
}
|
||||
async #getFileUrl(trackId: number | string, quality = 'HI_RES_LOSSLESS'): Promise<GetStreamResponse> {
|
||||
async #getFileUrl(
|
||||
trackId: number | string,
|
||||
quality = 'HI_RES_LOSSLESS'
|
||||
): Promise<GetStreamResponse> {
|
||||
interface PlaybackInfo {
|
||||
manifest: string
|
||||
manifestMimeType: string
|
||||
@ -413,7 +420,9 @@ export default class Tidal implements Streamer {
|
||||
}
|
||||
async getAccountInfo(): Promise<StreamerAccount> {
|
||||
if (!this.userId) await this.getCountryCode()
|
||||
const subscription = <SubscriptionData>await this.#get(`users/${this.userId}/subscription`, {}, TIDAL_SUBSCRIPTION_BASE)
|
||||
const subscription = <SubscriptionData>(
|
||||
await this.#get(`users/${this.userId}/subscription`, {}, TIDAL_SUBSCRIPTION_BASE)
|
||||
)
|
||||
|
||||
return {
|
||||
valid: true,
|
||||
|
||||
@ -174,7 +174,7 @@ export function parseMpd(mpdString: string): string[] {
|
||||
if (!initializationUrl) throw new Error('No initialization url')
|
||||
const mediaUrl = segTemplate.getAttribute('media')
|
||||
if (!mediaUrl) throw new Error('No media url')
|
||||
let trackUrls = [initializationUrl]
|
||||
const trackUrls = [initializationUrl]
|
||||
const timeline = segTemplate.querySelector('SegmentTimeline')
|
||||
if (timeline) {
|
||||
let numSegments = 0
|
||||
|
||||
10
src/types.ts
10
src/types.ts
@ -29,8 +29,8 @@ export interface Album {
|
||||
coverArtwork?: CoverArtwork[]
|
||||
artists?: Artist[]
|
||||
description?: string
|
||||
copyright?: string,
|
||||
label?: string,
|
||||
copyright?: string
|
||||
label?: string
|
||||
genre?: string[]
|
||||
regions?: string[]
|
||||
}
|
||||
@ -102,7 +102,7 @@ export interface GetStreamResponse {
|
||||
export type GetByUrlResponse =
|
||||
| TrackGetByUrlResponse
|
||||
| ArtistGetByUrlResponse
|
||||
| AlbumGetByUrlResponse
|
||||
| AlbumGetByUrlResponse
|
||||
| EpisodeGetByUrlResponse
|
||||
| PodcastGetByUrlResponse
|
||||
|
||||
@ -136,7 +136,7 @@ export interface StreamerAccount {
|
||||
premium?: boolean
|
||||
country?: string
|
||||
explicit?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
export interface Streamer {
|
||||
hostnames: string[]
|
||||
@ -151,4 +151,4 @@ export interface Streamer {
|
||||
|
||||
export interface StreamerWithLogin extends Streamer {
|
||||
login(username: string, password: string): Promise<void>
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user