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.
5.2 kB · 152 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153import dayjs from "dayjs";import type { EventEmitter } from "events";import normalizeUrl from 'normalize-url';import { URL } from "url";import {COMPONENT_AUTH_TYPE, type ComponentAuthType, type PlayObject, type PlayObjectMinimal} from "../../core/Atomic.ts";import type {FormatPlayObjectOptions, InternalConfig} from "../common/infrastructure/Atomic.ts";import type {JRiverSourceConfig} from "../common/infrastructure/config/source/jriver.ts";import { type Info, JRiverApiClient, PLAYER_STATE } from "../common/vendor/JRiverApiClient.ts";import type {RecentlyPlayedOptions} from "./AbstractSource.ts";import { MemoryPositionalSource } from "./MemoryPositionalSource.ts";import { baseFormatPlayObj } from "../utils/PlayTransformUtils.ts";import { artistNamesToCredits } from "../../core/StringUtils.ts";
export class JRiverSource extends MemoryPositionalSource { declare config: JRiverSourceConfig;
url: URL;
client: JRiverApiClient; clientReady: boolean = false; override authType: ComponentAuthType = COMPONENT_AUTH_TYPE.unattended;
constructor(name: any, config: JRiverSourceConfig, internal: InternalConfig, emitter: EventEmitter) { const { data, } = config; const { ...rest } = data || {}; super('jriver', name, {...config, data: {...rest}}, internal, emitter);
const { data: { url = 'http://localhost:52199/MCWS/v1/' } = {}, } = config; this.url = JRiverSource.parseConnectionUrl(url); this.client = new JRiverApiClient(name, {...data, url: this.url.toString()}, {logger: this.logger}); this.requiresAuth = true; this.canPoll = true; this.multiPlatform = true; }
static parseConnectionUrl(val: string) { const normal = normalizeUrl(val, {removeTrailingSlash: true, normalizeProtocol: true}) const url = new URL(normal);
if (url.port === null || url.port === '') { url.port = '52199'; } if (url.pathname === '/') { url.pathname = '/MCWS/v1/'; } else if (url.pathname === '/MCWS/v1') { url.pathname = '/MCWS/v1/'; } return url; }
protected async doBuildInitData(): Promise<true | string | undefined> { const { data: { url } = {} } = this.config; this.logger.verbose(`Config URL: '${url ?? '(None Given)'}' => Normalized: '${this.url.toString()}'`) return true; }
protected async doCheckConnection(): Promise<true | string | undefined> { try { return await this.client.testConnection(); } catch (e) { throw e; } }
doAuthentication = async () => { try { const resp = await this.client.testAuth(); this.clientReady = true; return true; } catch (e) { throw e; } }
static formatPlayObj(obj: Info, options: FormatPlayObjectOptions & {version?: string} = {}): PlayObject { const {newFromSource = true} = options;
const { Artist, Album, Name, DurationMS, PositionMS: trackProgressPosition, FileKey, ZoneID, ZoneName, } = obj;
const artists = Artist === null || Artist === undefined ? [] : [Artist]; const album = Album === null || Album === '' ? undefined : Album; const length = Number.parseInt(DurationMS.toString()) / 1000;
const play: PlayObjectMinimal = { data: { track: Name, album: album, artists: artistNamesToCredits(artists), duration: Math.round(length), playDate: dayjs() }, meta: { source: 'mopidy', mediaPlayerName: 'JRiver', mediaPlayerVersion: options.version, trackId: FileKey, newFromSource, trackProgressPosition: trackProgressPosition !== undefined ? Math.round(Number.parseInt(trackProgressPosition.toString()) / 1000) : undefined, deviceId: `Zone${ZoneID}${ZoneName !== undefined ? `-${ZoneName}` : ''}`, } } return baseFormatPlayObj(obj, play); }
getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => { if (!this.clientReady) { this.logger.warn('Cannot actively poll since client is not connected.'); return []; }
let play: PlayObject[] = [];
//should it use zones? //const zoneResp = await this.client.getZones(); const infoResp = await this.client.getInfo(); const { body: { data, } = {} } = infoResp; if(data !== undefined) { const {State} = data; if(State !== PLAYER_STATE.STOPPED) { play = [JRiverSource.formatPlayObj(data, {version: this.client.version})]; } }
return await this.processRecentPlays(play); }
}