1+ import { MemoryPositionalSource } from "./MemoryPositionalSource.js" ;
2+ import { RecentlyPlayedOptions } from "./AbstractSource.js" ;
3+ import { EventEmitter } from "events" ;
4+ import { PlayObject , URLData } from "../../core/Atomic.js" ;
5+ import {
6+ FormatPlayObjectOptions ,
7+ InternalConfig ,
8+ PlayerStateData ,
9+ SINGLE_USER_PLATFORM_ID ,
10+ } from "../common/infrastructure/Atomic.js" ;
11+ import { isPortReachable , joinedUrl , normalizeWebAddress } from "../utils/NetworkUtils.js" ;
12+ import { DeviceInfoResponse , DeviceStatusResponse , MusicCastSourceConfig , playbackToReportedStatus , PlayInfoCDResponse , PlayInfoNetResponse } from "../common/infrastructure/config/source/musiccast.js" ;
13+ import request , { Request , Response } from 'superagent' ;
14+
15+
16+ export class MusicCastSource extends MemoryPositionalSource {
17+
18+ declare config : MusicCastSourceConfig ;
19+
20+ urlData ! : URLData ;
21+
22+
23+ constructor ( name : any , config : MusicCastSourceConfig , internal : InternalConfig , emitter : EventEmitter ) {
24+ const {
25+ data = { }
26+ } = config ;
27+ const {
28+ ...rest
29+ } = data ;
30+ super ( 'musiccast' , name , { ...config , data : { ...rest } } , internal , emitter ) ;
31+
32+ this . requiresAuth = false ;
33+ this . canPoll = true ;
34+ }
35+
36+ protected async doBuildInitData ( ) : Promise < true | string | undefined > {
37+ const {
38+ data : {
39+ url
40+ } = { }
41+ } = this . config ;
42+ if ( url === null || url === undefined || url === '' ) {
43+ throw new Error ( 'url must be defined' ) ;
44+ }
45+ this . urlData = normalizeWebAddress ( url , { defaultPath : '/YamahaExtendedControl/v1' } ) ;
46+ const normal = this . urlData . normal ;
47+ this . logger . verbose ( `Config URL: '${ url ?? '(None Given)' } ' => Normalized: '${ normal } '` )
48+ return true ;
49+ }
50+
51+
52+ protected async doCheckConnection ( ) : Promise < true | string | undefined > {
53+ try {
54+ await isPortReachable ( this . urlData . port , { host : this . urlData . url . hostname } ) ;
55+ this . logger . verbose ( `${ this . urlData . url . hostname } :${ this . urlData . port } is reachable.` ) ;
56+
57+ const resp = await request . get ( joinedUrl ( this . urlData . url , 'system/getDeviceInfo' ) . toString ( ) )
58+ if ( resp . body !== undefined && typeof resp . body === 'object' ) {
59+ const deviceInfo = resp . body as DeviceInfoResponse ;
60+ this . logger . info ( `Found ${ deviceInfo . model_name } (${ deviceInfo . device_id } ) using API v${ deviceInfo . api_version } ` ) ;
61+ } else {
62+ this . logger . warn ( 'Could not get device info! Ignoring but probably not good...' ) ;
63+ }
64+ return true ;
65+ } catch ( e ) {
66+ const hint = e . error ?. cause ?. message ?? undefined ;
67+ throw new Error ( `Could not connect to MusicCast server${ hint !== undefined ? ` (${ hint } )` : '' } ` , { cause : e . error ?? e } ) ;
68+ }
69+ }
70+
71+ getAnyPlayInfo = async ( ) : Promise < PlayInfoCDResponse | PlayInfoNetResponse | undefined > => {
72+ try {
73+ const cdResp = await request . get ( joinedUrl ( this . urlData . url , '/cd/getPlayInfo' ) . toString ( ) ) ;
74+ if ( cdResp . body !== undefined && typeof cdResp . body === 'object' ) {
75+ return cdResp . body as PlayInfoCDResponse ;
76+ }
77+ } catch ( e ) {
78+ this . logger . warn ( new Error ( 'Not OK response from cd getPlayInfo but will continue' , { cause : e } ) ) ;
79+ }
80+
81+ try {
82+ const netResp = await request . get ( joinedUrl ( this . urlData . url , '/netusb/getPlayInfo' ) . toString ( ) ) ;
83+ if ( netResp . body !== undefined && typeof netResp . body === 'object' ) {
84+ return netResp . body as PlayInfoNetResponse
85+ }
86+ } catch ( e ) {
87+ this . logger . warn ( new Error ( 'Not OK response from netusb getPlayInfo but will continue' , { cause : e } ) ) ;
88+ }
89+
90+ return undefined ;
91+ }
92+
93+ getRecentlyPlayed = async ( options : RecentlyPlayedOptions = { } ) => {
94+
95+ const statusResp = await request . get ( joinedUrl ( this . urlData . url , 'main/getStatus' ) . toString ( ) ) ;
96+ if ( statusResp . body == undefined || typeof statusResp . body !== 'object' ) {
97+ this . logger . error ( { getStatusResponse : statusResp } ) ;
98+ throw new Error ( 'Could not determine status of MusicCast device' ) ;
99+ }
100+ if ( ( statusResp . body as DeviceStatusResponse ) . power !== 'on' ) {
101+ this . logger . debug ( 'MusicCast device is offline' ) ;
102+ return this . processRecentPlays ( [ ] ) ;
103+ }
104+
105+ const playInfo = await this . getAnyPlayInfo ( ) ;
106+ if ( playInfo === undefined ) {
107+ return this . processRecentPlays ( [ ] ) ;
108+ }
109+
110+ const play = formatPlayObj ( playInfo ) ;
111+
112+
113+ const playerState : PlayerStateData = {
114+ platformId : SINGLE_USER_PLATFORM_ID ,
115+ status : playbackToReportedStatus ( playInfo . playback ) ,
116+ play,
117+ position : play . meta . trackProgressPosition
118+ }
119+
120+ return this . processRecentPlays ( [ playerState ] ) ;
121+ }
122+ }
123+
124+ const formatPlayObj = ( obj : PlayInfoCDResponse | PlayInfoNetResponse , options : FormatPlayObjectOptions = { } ) : PlayObject => {
125+
126+ const {
127+ play_time,
128+ total_time,
129+ artist,
130+ album,
131+ track,
132+ device_status,
133+ playback
134+ } = obj ;
135+
136+ return {
137+ data : {
138+ artists : artist !== undefined && artist !== '' ? [ artist ] : [ ] ,
139+ album : album !== '' ? album : undefined ,
140+ track,
141+ duration : total_time
142+ } ,
143+ meta : {
144+ trackProgressPosition : play_time ,
145+ deviceId : 'input' in obj ? obj . input : 'cd'
146+ }
147+ }
148+ }
0 commit comments