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.
6.9 kB · 167 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167import { MemoryPositionalSource } from "./MemoryPositionalSource.ts";import type {RecentlyPlayedOptions} from "./AbstractSource.ts";import type { EventEmitter } from "events";import type {PlayObject, PlayObjectMinimal, URLData} from "../../core/Atomic.ts";import { type FormatPlayObjectOptions, type InternalConfig, type PlayerStateData,} from "../common/infrastructure/Atomic.ts";import { SINGLE_USER_PLATFORM_ID } from '../../core/Atomic.ts';import { isPortReachableConnect, joinedUrl, normalizeWebAddress } from "../utils/NetworkUtils.ts";import { type DeviceInfoResponse, type DeviceStatusResponse, MusicCastResponseCodes, type MusicCastSourceConfig, playbackToReportedStatus, type PlayInfoCDResponse, type PlayInfoNetResponse } from "../common/infrastructure/config/source/musiccast.ts";import request from 'superagent';import { baseFormatPlayObj } from "../utils/PlayTransformUtils.ts";import { artistNamesToCredits } from "../../core/StringUtils.ts";
export class MusicCastSource extends MemoryPositionalSource {
declare config: MusicCastSourceConfig;
urlData!: URLData; version?: string;
constructor(name: any, config: MusicCastSourceConfig, internal: InternalConfig, emitter: EventEmitter) { const { data = {} } = config; const { ...rest } = data; super('musiccast', name, { ...config, data: { ...rest } }, internal, emitter);
this.requiresAuth = false; this.canPoll = true; }
protected async doBuildInitData(): Promise<true | string | undefined> { const { data: { url } = {} } = this.config; if (url === null || url === undefined || url === '') { throw new Error('url must be defined'); } this.urlData = normalizeWebAddress(url, { defaultPath: '/YamahaExtendedControl/v1' }); const normal = this.urlData.normal; this.logger.verbose(`Config URL: '${url ?? '(None Given)'}' => Normalized: '${normal}'`) return true; }
protected async doCheckConnection(): Promise<true | string | undefined> { try { await isPortReachableConnect(this.urlData.port, { host: this.urlData.url.hostname }); this.logger.verbose(`${this.urlData.url.hostname}:${this.urlData.port} is reachable.`);
const resp = await request.get(joinedUrl(this.urlData.url, 'system/getDeviceInfo').toString()) if (resp.body !== undefined && typeof resp.body === 'object') { const deviceInfo = resp.body as DeviceInfoResponse; if(deviceInfo.api_version !== undefined) { this.version = deviceInfo.api_version.toString(); } this.logger.info(`Found ${deviceInfo.model_name} (${deviceInfo.device_id}) using API v${deviceInfo.api_version}`); } else { this.logger.warn('Could not get device info! Ignoring but probably not good...'); } return true; } catch (e) { const hint = e.error?.cause?.message ?? undefined; throw new Error(`Could not connect to MusicCast server${hint !== undefined ? ` (${hint})` : ''}`, { cause: e.error ?? e }); } }
getAnyPlayInfo = async (): Promise<PlayInfoCDResponse | PlayInfoNetResponse | undefined> => {
try { const netResp = await request.get(joinedUrl(this.urlData.url, '/netusb/getPlayInfo').toString()); if (netResp.body !== undefined && typeof netResp.body === 'object') { const resp = netResp.body as PlayInfoNetResponse if(resp.response_code !== 0) { throw new Error(`netusb source is unexpected status: ${resp.response_code} (${MusicCastResponseCodes.get(resp.response_code) ?? 'Unknown'})`); } return resp; } } catch (e) { this.logger.warn(new Error('Not OK response from netusb getPlayInfo but will continue', {cause: e})); }
try { const cdResp = await request.get(joinedUrl(this.urlData.url, '/cd/getPlayInfo').toString()); if (cdResp.body !== undefined && typeof cdResp.body === 'object') { const resp = cdResp.body as PlayInfoCDResponse; if(resp.response_code !== 0) { throw new Error(`cd source is unexpected status: ${resp.response_code} (${MusicCastResponseCodes.get(resp.response_code) ?? 'Unknown'})`); } return resp; } } catch (e) { this.logger.warn(new Error('Not OK response from cd getPlayInfo but will continue', {cause: e})); }
return undefined; }
getRecentlyPlayed = async (options: RecentlyPlayedOptions = {}) => {
const statusResp = await request.get(joinedUrl(this.urlData.url, 'main/getStatus').toString()); if (statusResp.body == undefined || typeof statusResp.body !== 'object') { this.logger.error({ getStatusResponse: statusResp }); throw new Error('Could not determine status of MusicCast device'); } if ((statusResp.body as DeviceStatusResponse).power !== 'on') { this.logger.debug('MusicCast device is offline'); return await this.processRecentPlays([]); }
const playInfo = await this.getAnyPlayInfo(); if(playInfo === undefined) { return await this.processRecentPlays([]); }
const play = formatPlayObj(playInfo);
const playerState: PlayerStateData = { platformId: SINGLE_USER_PLATFORM_ID, status: playbackToReportedStatus(playInfo.playback), play, position: play.meta.trackProgressPosition }
return await this.processRecentPlays([playerState]); }}
const formatPlayObj = (obj: PlayInfoCDResponse | PlayInfoNetResponse, options: FormatPlayObjectOptions & {version?: string} = {}): PlayObject => {
const { play_time, total_time, artist, album, track, device_status, playback } = obj;
const play: PlayObjectMinimal = { data: { artists: artistNamesToCredits(artist !== undefined && artist !== '' ? [artist] : []), album: album !== '' ? album : undefined, track, // we should treat 0 time as the same as not being provided duration: total_time === 0 ? undefined : total_time }, meta: { trackProgressPosition: play_time, deviceId: 'input' in obj ? obj.input : 'cd', mediaPlayerName: 'MusicCast', mediaPlayerVersion: options.version } } return baseFormatPlayObj(obj, play);}