Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/FoxxMD/multi-scrobbler. Scrobble plays from multiple sources to multiple clients docs.multi-scrobbler.app
deezer docker jellyfin koito lastfm listenbrainz maloja mopidy mpris music music-assistant plex scrobble self-hosted spotify subsonic tautulli youtube-music
Something went wrong. Try again.
4.7 kB · 170 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171import type { Request, Response } from 'superagent';import request from 'superagent';import xml2js from 'xml2js';import { type AbstractApiOptions, DEFAULT_RETRY_MULTIPLIER } from "../infrastructure/Atomic.ts";import type {JRiverData} from "../infrastructure/config/source/jriver.ts";import AbstractApiClient from "./AbstractApiClient.ts";import { AuthError } from '../errors/MSErrors.ts';
const parser = new xml2js.Parser({'async': true});
export const PLAYER_STATE: Record<string, PLAYER_STATE> = { STOPPED: '0', PAUSED: '1', PLAYING: '2'}
export type PLAYER_STATE = '0' | '1' | '2';
interface JRiverResponseItem { _: string $: { Name: string }}
interface JRiverResponse { Response: { '$': { Status: string }, Item: JRiverResponseItem[] }}
export interface JRiverTransformedResponse<T> { status: string data?: T}
export interface Alive { RuntimeGUID: string LibraryVersion: string ProgramName: string ProgramVersion: string FriendlyName: string AccessKey: string ProductVersion: string Platform: string}// state 0 = nothing?// 2 = playing// 1 = paused
export interface Authenticate { Token: string ReadOnly: number PreLicensed: boolean}
export interface Info { ZoneID: string ZoneName: string State: PLAYER_STATE PositionMS: number DurationMS: number Artist: string Album: string Name: string Status: string FileKey: string}
export interface Zones { NumberZones: number CurrentZoneID: string CurrentZoneIndex: string}
const jriverResponseTransform = <T>(val: JRiverResponse): JRiverTransformedResponse<T> => { const status = val.Response.$.Status; const items = val.Response.Item === undefined ? undefined : val.Response.Item.map(x => { return [x.$.Name, x._]; }); return { status, data: (items ?? []).reduce((acc: Record<string, string>, curr) => { acc[curr[0]] = curr[1]; return acc; }, {}) as T };}
export class JRiverApiClient extends AbstractApiClient {
declare config: JRiverData
url: string;
token?: string;
version?: string;
constructor(name: any, config: JRiverData, options: AbstractApiOptions) { super('JRiver', name, config, options); const { url = 'http://localhost:52199/MCWS/v1/' } = config; this.url = url; }
callApi = async <T>(req: Request, retries = 0): Promise<Response & {body: T}> => { const { maxRequestRetries = 2, retryMultiplier = DEFAULT_RETRY_MULTIPLIER } = this.config;
if (this.token !== undefined) { req.query({token: this.token}); }
try { const resp = await req as Response; if (resp.text !== '') { const rawBody = await parser.parseStringPromise(resp.text); resp.body = jriverResponseTransform(rawBody); } return resp; } catch (e) { throw e; } }
testConnection = async (): Promise<true> => { try { const resp = await this.callApi<Alive>(request.get(`${this.url}Alive`)); const {body: { data } = {}} = resp; this.version = data.ProgramVersion; this.logger.verbose(`Found ${data.ProgramName} ${data.ProgramVersion} (${data.FriendlyName})`); return true; } catch (e) { throw new Error('Could not communicate with JRiver server. Verify your server URL is correct.', {cause: e}); } }
testAuth = async () => { try { const req = request.get(`${this.url}Authenticate`); if (this.config.username !== undefined) { req.auth(this.config.username, this.config.password as string); } const resp = await this.callApi<Authenticate>(req); this.token = resp.body.data.Token; return true; } catch (e) { let msg = 'Authentication failed.'; if(this.config.username === undefined || this.config.password === undefined) { msg = 'Authentication failed. No username/password was provided in config! Did you mean to do this?'; } throw new AuthError(msg, {cause: e}); } }
getInfo = async (zoneId: string = '-1') => { return await this.callApi<Info>(request.get(`${this.url}Playback/Info`).query({Zone: zoneId})); }
getZones = async () => { return await this.callApi<Zones>(request.get(`${this.url}Playback/Zones`)); }}