mirror of
https://codeberg.org/lucida/lucida.git
synced 2026-11-14 23:57:27 -05:00
add tests
This commit is contained in:
26
.forgejo/workflows/check.yml
Normal file
26
.forgejo/workflows/check.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check:
|
||||||
|
runs-on: docker
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: https://code.forgejo.org/actions/checkout@v4
|
||||||
|
- uses: https://github.com/pnpm/action-setup@v4
|
||||||
|
name: Install pnpm
|
||||||
|
with:
|
||||||
|
version: 9
|
||||||
|
run_install: false
|
||||||
|
- name: Install Node.js
|
||||||
|
uses: https://github.com/actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 'latest'
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm install
|
||||||
|
- run: pnpm run build
|
||||||
|
- run: pnpm run test
|
||||||
|
- name: Comment test results
|
||||||
|
uses: https://github.com/thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
filePath: '.test-results.md'
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,4 +11,7 @@ node_modules
|
|||||||
/tmp
|
/tmp
|
||||||
/run.js
|
/run.js
|
||||||
|
|
||||||
|
.test-options.json
|
||||||
|
.test-results.*
|
||||||
|
|
||||||
.pnpm-store
|
.pnpm-store
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
"types": "build/index.d.ts",
|
"types": "build/index.d.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
|
"build": "tsc",
|
||||||
|
"watch": "tsc -w",
|
||||||
|
"test": "node build/test.js",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
"import": "./build/types.js",
|
"import": "./build/types.js",
|
||||||
"types": "./build/types.d.ts"
|
"types": "./build/types.d.ts"
|
||||||
},
|
},
|
||||||
|
"./streamers/*": "./build/streamers/*/main.js",
|
||||||
"./streamers/*.js": "./build/streamers/*.js"
|
"./streamers/*.js": "./build/streamers/*.js"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
|||||||
17
src/index.ts
17
src/index.ts
@@ -7,7 +7,7 @@ import {
|
|||||||
StreamerAccount
|
StreamerAccount
|
||||||
} from './types.js'
|
} from './types.js'
|
||||||
|
|
||||||
interface LucidaOptions {
|
export interface LucidaOptions {
|
||||||
modules: { [key: string]: Streamer | StreamerWithLogin }
|
modules: { [key: string]: Streamer | StreamerWithLogin }
|
||||||
logins?: {
|
logins?: {
|
||||||
[key: string]: {
|
[key: string]: {
|
||||||
@@ -28,13 +28,24 @@ class Lucida {
|
|||||||
.flat()
|
.flat()
|
||||||
if (options.logins) this.logins = options.logins
|
if (options.logins) this.logins = options.logins
|
||||||
}
|
}
|
||||||
async login() {
|
async login(ignoreFailures = false) {
|
||||||
if (!this.logins) throw new Error('No logins specified')
|
if (!this.logins) throw new Error('No logins specified')
|
||||||
for (const i in this.modules) {
|
for (const i in this.modules) {
|
||||||
const credentials = this.logins[i]
|
const credentials = this.logins[i]
|
||||||
const module = this.modules[i]
|
const module = this.modules[i]
|
||||||
if (module && 'login' in module) {
|
if (module && 'login' in module) {
|
||||||
await module.login?.(credentials?.username, credentials?.password)
|
try {
|
||||||
|
await module.login?.(credentials?.username, credentials?.password)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
if (!ignoreFailures) {
|
||||||
|
throw new Error(`Failed to login to ${i}`)
|
||||||
|
} else {
|
||||||
|
await module.disconnect?.()
|
||||||
|
console.error(`ignoreFailures is on, removing ${i} module...`)
|
||||||
|
delete this.modules[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,20 @@ interface APIMethod {
|
|||||||
|
|
||||||
export default class Deezer implements StreamerWithLogin {
|
export default class Deezer implements StreamerWithLogin {
|
||||||
hostnames = ['deezer.com', 'www.deezer.com', 'deezer.page.link']
|
hostnames = ['deezer.com', 'www.deezer.com', 'deezer.page.link']
|
||||||
|
testData = {
|
||||||
|
'https://www.deezer.com/us/artist/1194083': {
|
||||||
|
type: 'artist',
|
||||||
|
title: 'Tyler, The Creator'
|
||||||
|
},
|
||||||
|
'https://www.deezer.com/us/track/559711712': {
|
||||||
|
type: 'track',
|
||||||
|
title: 'Potato Salad'
|
||||||
|
},
|
||||||
|
'https://www.deezer.com/us/album/97140952': {
|
||||||
|
type: 'album',
|
||||||
|
title: 'IGOR'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
headers: { [header: string]: string } = {
|
headers: { [header: string]: string } = {
|
||||||
Accept: '*/*',
|
Accept: '*/*',
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ interface LoginResponse {
|
|||||||
|
|
||||||
export default class Qobuz implements StreamerWithLogin {
|
export default class Qobuz implements StreamerWithLogin {
|
||||||
hostnames = ['play.qobuz.com', 'open.qobuz.com', 'www.qobuz.com', 'qobuz.com']
|
hostnames = ['play.qobuz.com', 'open.qobuz.com', 'www.qobuz.com', 'qobuz.com']
|
||||||
|
testData = {
|
||||||
|
'https://www.qobuz.com/us-en/interpreter/tyler-the-creator/589771': {
|
||||||
|
title: 'Tyler, The Creator',
|
||||||
|
type: 'artist'
|
||||||
|
},
|
||||||
|
'https://www.qobuz.com/us-en/album/igor-tyler-the-creator/qtz65tw2of0ha': {
|
||||||
|
title: 'IGOR',
|
||||||
|
type: 'album'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
token?: string
|
token?: string
|
||||||
appSecret: string
|
appSecret: string
|
||||||
appId: string
|
appId: string
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ interface SoundcloudTranscoding {
|
|||||||
|
|
||||||
export default class Soundcloud implements Streamer {
|
export default class Soundcloud implements Streamer {
|
||||||
hostnames = ['soundcloud.com', 'm.soundcloud.com', 'www.soundcloud.com']
|
hostnames = ['soundcloud.com', 'm.soundcloud.com', 'www.soundcloud.com']
|
||||||
|
testData = {
|
||||||
|
'https://soundcloud.com/saoirsedream/charlikartlanparty': {
|
||||||
|
type: 'track',
|
||||||
|
title: 'Charli Kart LAN Party'
|
||||||
|
},
|
||||||
|
'https://soundcloud.com/saoirsedream/sets/star': {
|
||||||
|
type: 'album',
|
||||||
|
title: 'star★☆'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
oauthToken?: string
|
oauthToken?: string
|
||||||
client?: ScClient
|
client?: ScClient
|
||||||
constructor(options: SoundcloudOptions) {
|
constructor(options: SoundcloudOptions) {
|
||||||
|
|||||||
@@ -11,6 +11,21 @@ import {
|
|||||||
class Spotify implements StreamerWithLogin {
|
class Spotify implements StreamerWithLogin {
|
||||||
client: Librespot
|
client: Librespot
|
||||||
hostnames = ['open.spotify.com']
|
hostnames = ['open.spotify.com']
|
||||||
|
testData = {
|
||||||
|
'https://open.spotify.com/track/1jzIJcHCXneHw7ojC6LXiF': {
|
||||||
|
type: 'track',
|
||||||
|
title: 'Potato Salad'
|
||||||
|
},
|
||||||
|
'https://open.spotify.com/album/5zi7WsKlIiUXv09tbGLKsE': {
|
||||||
|
type: 'album',
|
||||||
|
title: 'IGOR'
|
||||||
|
},
|
||||||
|
'https://open.spotify.com/artist/4V8LLVI7PbaPR0K2TGSxFF': {
|
||||||
|
type: 'artist',
|
||||||
|
title: 'Tyler, The Creator'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
|
|
||||||
constructor(options: LibrespotOptions) {
|
constructor(options: LibrespotOptions) {
|
||||||
this.client = new Librespot(options)
|
this.client = new Librespot(options)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,20 @@ export default class Tidal implements Streamer {
|
|||||||
countryCode: string
|
countryCode: string
|
||||||
userId: number | undefined
|
userId: number | undefined
|
||||||
hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com']
|
hostnames = ['tidal.com', 'www.tidal.com', 'listen.tidal.com']
|
||||||
|
testData = {
|
||||||
|
'https://tidal.com/browse/artist/3908662': {
|
||||||
|
type: 'artist',
|
||||||
|
title: 'Tyler, The Creator'
|
||||||
|
},
|
||||||
|
'https://tidal.com/browse/album/109485854': {
|
||||||
|
type: 'album',
|
||||||
|
title: 'IGOR'
|
||||||
|
},
|
||||||
|
'https://tidal.com/browse/track/95691774': {
|
||||||
|
type: 'track',
|
||||||
|
title: 'Potato Salad'
|
||||||
|
}
|
||||||
|
} as const
|
||||||
failedAuth = false
|
failedAuth = false
|
||||||
constructor(options: TidalOptions) {
|
constructor(options: TidalOptions) {
|
||||||
this.tvToken = options.tvToken
|
this.tvToken = options.tvToken
|
||||||
|
|||||||
152
src/test.ts
Normal file
152
src/test.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import Lucida, { LucidaOptions } from 'lucida'
|
||||||
|
import { GetByUrlResponse, Streamer } from 'lucida/types'
|
||||||
|
|
||||||
|
import fs from 'node:fs'
|
||||||
|
|
||||||
|
let testOptions: {
|
||||||
|
[module: string]: {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
[key: string]: string | number | boolean | undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!process.env.TEST_OPTIONS) {
|
||||||
|
console.error(
|
||||||
|
'No test options found in environment variables. Trying to read from .test-options.json'
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
const testOptionsFile = fs.readFileSync('.test-options.json', 'utf-8')
|
||||||
|
testOptions = JSON.parse(testOptionsFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'No test options found in environment variables or .test-options.json. Exiting...'
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testOptions = JSON.parse(process.env.TEST_OPTIONS)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function start() {
|
||||||
|
const lucidaOptions: LucidaOptions = {
|
||||||
|
modules: {}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [module, options] of Object.entries(testOptions)) {
|
||||||
|
const username = options.username
|
||||||
|
const password = options.password
|
||||||
|
delete options.username
|
||||||
|
delete options.password
|
||||||
|
const { default: Streamer } = await import(`lucida/streamers/${module}`)
|
||||||
|
lucidaOptions.modules[module] = <Streamer>new Streamer(options)
|
||||||
|
if (username && password) {
|
||||||
|
lucidaOptions.logins ??= {}
|
||||||
|
lucidaOptions.logins[module] = { username, password }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const lucida = new Lucida(lucidaOptions)
|
||||||
|
await lucida.login(true)
|
||||||
|
|
||||||
|
const testResults: {
|
||||||
|
[module: string]: {
|
||||||
|
loginSuccess: boolean
|
||||||
|
searchSuccess: boolean
|
||||||
|
urlTestsSuccess?: number
|
||||||
|
}
|
||||||
|
} = {}
|
||||||
|
|
||||||
|
for (const moduleName of Object.keys(testOptions)) {
|
||||||
|
const module = lucida.modules[moduleName]
|
||||||
|
console.log(`Testing ${moduleName}...`)
|
||||||
|
testResults[moduleName] ??= {
|
||||||
|
loginSuccess: module ? true : false,
|
||||||
|
searchSuccess: false
|
||||||
|
}
|
||||||
|
if (!module) {
|
||||||
|
console.error(`Module ${moduleName} not found in Lucida instance. Skipping...`)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await Promise.race([
|
||||||
|
module.search('test', 5),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject('Search timed out'), 10000))
|
||||||
|
])
|
||||||
|
console.log(`Searching with the ${moduleName} streamer succeeded`)
|
||||||
|
testResults[moduleName].searchSuccess = true
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Searching with the ${moduleName} streamer failed:`, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
let urlTestsSuccess = 0
|
||||||
|
if (module.testData) {
|
||||||
|
for (const url in module.testData) {
|
||||||
|
const expected = module.testData[url]
|
||||||
|
let getResult: GetByUrlResponse
|
||||||
|
try {
|
||||||
|
getResult = <GetByUrlResponse>(
|
||||||
|
await Promise.race([
|
||||||
|
module.getByUrl(url),
|
||||||
|
new Promise((_, reject) => setTimeout(() => reject('Search timed out'), 10000))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Getting data from ${url} with the ${moduleName} streamer failed:`, error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const actual = {
|
||||||
|
title: getResult.type != 'artist' ? getResult.metadata.title : getResult.metadata.name,
|
||||||
|
type: getResult.type
|
||||||
|
}
|
||||||
|
const success = {
|
||||||
|
title: actual.title.toLowerCase() == expected.title.toLowerCase(),
|
||||||
|
type: actual.type == expected.type
|
||||||
|
}
|
||||||
|
if (!success.type) {
|
||||||
|
console.error(
|
||||||
|
`Type mismatch for ${url} with the ${moduleName} streamer. Expected: ${expected.title}, got: ${actual.title}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!success.title) {
|
||||||
|
console.error(
|
||||||
|
`Title mismatch for ${url} with the ${moduleName} streamer. Expected: ${expected.title}, got: ${actual.title}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (success.title && success.type) {
|
||||||
|
console.log(`getByUrl test succeeded`)
|
||||||
|
urlTestsSuccess++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
testResults[moduleName].urlTestsSuccess =
|
||||||
|
urlTestsSuccess / Object.keys(module.testData).length
|
||||||
|
} else {
|
||||||
|
console.error(`No test data found for ${moduleName}. Skipping URL tests...`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lucida.disconnect()
|
||||||
|
|
||||||
|
console.log('Testing complete. Results:', testResults)
|
||||||
|
|
||||||
|
const readableResults = Object.entries(testResults).map((result) => {
|
||||||
|
const moduleName = result[0]
|
||||||
|
const moduleResults = result[1]
|
||||||
|
const score = Object.values(moduleResults).filter(
|
||||||
|
(result) => result === 1 || result != false
|
||||||
|
).length
|
||||||
|
|
||||||
|
const percentage = (score / Object.keys(moduleResults).length) * 100
|
||||||
|
return `- ${moduleName}: ${percentage.toFixed(2)}%`
|
||||||
|
})
|
||||||
|
|
||||||
|
fs.writeFileSync(
|
||||||
|
'.test-results.md',
|
||||||
|
`## Testing complete\n\n${readableResults.join('\n')}\n\n<details><summary>Results</summary>\n\n\`\`\`json\n` +
|
||||||
|
JSON.stringify(testResults, null, '\t') +
|
||||||
|
'\n```\n\n</details>'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
start()
|
||||||
@@ -138,8 +138,16 @@ export interface StreamerAccount {
|
|||||||
explicit?: boolean
|
explicit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface StreamerTestData {
|
||||||
|
[url: string]: {
|
||||||
|
title: string
|
||||||
|
type: ItemType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export interface Streamer {
|
export interface Streamer {
|
||||||
hostnames: string[]
|
hostnames: string[]
|
||||||
|
testData?: StreamerTestData
|
||||||
search(query: string, limit: number): Promise<SearchResults>
|
search(query: string, limit: number): Promise<SearchResults>
|
||||||
getTypeFromUrl(url: string): Promise<ItemType>
|
getTypeFromUrl(url: string): Promise<ItemType>
|
||||||
getByUrl:
|
getByUrl:
|
||||||
|
|||||||
Reference in New Issue
Block a user