INTERACT FORUM

Please login or register.

Login with username, password and session length
Advanced search  
Pages: [1]   Go Down

Author Topic: Remote Control/Automation issues  (Read 2846 times)

RemyJ

  • Regular Member
  • Citizen of the Universe
  • *****
  • Posts: 1249
Remote Control/Automation issues
« on: May 16, 2021, 01:45:20 pm »

Besides fiddling with Library Servers, I'm also fiddling with my remote control/automation stuff.   I think these have been mentioned before but it's been a while so I thought I'd mention them again.  None of the /MCC,  /Play, /Pause, /Stop, etc command line options do anything other than give focus to the currently running instance (which is bad by itself).   /MCWS/* options do work but because they also give focus to the running instance, they're not really useful for automation.   

Using the MCWS via the network works fine of course but it does require that the media server be started.  That's fine but could we get an option to get the MCWS output in JSON rather than XML?  JSON is much easier to work with in most scripting languages.




Logged
Fedora 40 x86_64 Xfce

max096

  • MC Beta Team
  • Galactic Citizen
  • *****
  • Posts: 363
Re: Remote Control/Automation issues
« Reply #1 on: May 17, 2021, 06:15:34 pm »

For many things that list stuff you actually need to parse they do have query params to return it as JSON (not sure if for everything). If you set the "Action" query parameter to JSON you get back JSON for all endpoints that support that.

For Stop/Play etc. I don't think it's a huge deal since you can just see if the response is 200 status code and contains <Response Status="OK"/> no need to parse the xml there.

The CLI never really was implemented on the Linux version of MC.
Logged

RemyJ

  • Regular Member
  • Citizen of the Universe
  • *****
  • Posts: 1249
Re: Remote Control/Automation issues
« Reply #2 on: May 17, 2021, 07:19:46 pm »

If you set the "Action" query parameter to JSON you get back JSON for all endpoints that support that.

Unfortunately only a few endpoints support Action and the one I needed (Playback/Info) isn't one of them.
Logged
Fedora 40 x86_64 Xfce

Mike Noe

  • MC Beta Team
  • Citizen of the Universe
  • *****
  • Posts: 792
Re: Remote Control/Automation issues
« Reply #3 on: May 19, 2021, 10:53:21 am »

Here's some QML with the appropriate amount of JavaScript in the mix.  Perhaps you could find the javascript parts useful, specifically, XMLHttpRequest:
Code: [Select]
import QtQuick 2.8
import 'utils.js' as Utils

QtObject {

    property string currentHost
    property string hostUrl

    readonly property var forEach: Array.prototype.forEach

    onCurrentHostChanged: hostUrl = "http://%1/MCWS/v1/".arg(currentHost)

    signal connectionError(var msg, var cmd)
    signal commandError(var msg, var cmd)

    // Issue xhr mcws request, handle json/xml/text results
    function __exec(cmdstr, callback, json) {
        json = json !== undefined ? json : false

        var xhr = new XMLHttpRequest()

        xhr.onerror = () => {
            connectionError("Unable to connect: ", cmdstr)
        }

        xhr.onreadystatechange = () =>
        {
            if (xhr.readyState === XMLHttpRequest.DONE) {
                if (xhr.status === 200) {
                    if (Utils.isFunction(callback)) {
                        // FIXME: Remove MPL? Check return format
                        if (xhr.getResponseHeader('Content-Type') === 'application/x-mediajukebox-mpl')
                            console.log('MPL:', cmdstr)
                        if (xhr.getResponseHeader('Content-Type') === 'application/x-mediajukebox-mpl'
                                || xhr.getResponseHeader('Content-Type') === 'application/json')
                            callback(xhr.response)
                        else
                            callback(xhr.responseXML.documentElement.childNodes)
                    }
                } else {
                    if (xhr.getResponseHeader('Content-Type') !== 'application/x-mediajukebox-mpl'
                            & xhr.getResponseHeader('Content-Type') !== 'application/json')
                        commandError(xhr.responseXML
                                        ? xhr.responseXML.documentElement.attributes[1].value
                                        : 'Connection error'
                                     + ' <status: %1:%2>'.arg(xhr.status).arg(xhr.statusText), cmdstr)
                    else
                        commandError('<status: %1:%2>'.arg(xhr.status).arg(xhr.statusText), cmdstr)
                }
            }
        }

        xhr.open("GET", cmdstr);
        if (json)
            xhr.responseType = 'json'
        xhr.send();
    }

    // Load a model with Key/Value pairs
    function loadKVModel(cmd, model, callback) {
        if (model === undefined) {
            if (Utils.isFunction(callback))
                callback(0)
            return
        }

        __exec(hostUrl + cmd, nodes =>
        {
            // XML nodes, key = attr.name, value = node.value
            forEach.call(nodes, node =>
            {
                if (node.nodeType === 1) {
                    model.append({ key: Utils.toRoleName(node.attributes[0].value)
                                 , value: isNaN(node.firstChild.data)
                                          ? node.firstChild.data
                                          : +node.firstChild.data })
                }
            })
            if (Utils.isFunction(callback))
                callback(model.count)
        })
    }

    // Get array of objects, callback(array)
    function loadObject(cmd, callback) {
        __exec(hostUrl + cmd, (nodes) =>
        {
            // XML nodes, builds obj as single object with props = nodes
            if (Utils.isObject(nodes)) {
                var obj = {}
                forEach.call(nodes, node =>
                {
                    if (node.nodeType === 1 && node.firstChild !== null) {
                        let role = Utils.toRoleName(node.attributes[0].value)
                        obj[role] = role.includes('name') || isNaN(node.firstChild.data)
                                    ? node.firstChild.data
                                    : +node.firstChild.data
                    }
                })
                if (Utils.isFunction(callback))
                    callback(obj)
            // MPL string (multiple Items/multiple Fields for each item) builds an array of item objs
            } else if (typeof nodes === 'string') {
                var list = []
                __createObjectList(nodes, function(obj) { list.push(obj) })
                if (Utils.isFunction(callback))
                    callback(list)
            }
        })
    }

    // Get JSON array of objects, callback(array)
    function loadJSON(cmd, callback) {
        if (!Utils.isFunction(callback))
            return

        __exec(hostUrl + cmd, json =>
        {
           try {
               var arr = []
               json.forEach(item => {
                    let obj = {}
                    for (var p in item) {
                        obj[Utils.toRoleName(p)] = String(item[p])
                    }
                    arr.push(obj)
                })

               callback(arr)
           }
           catch (err) {
               console.log(commandError(err, 'JSON data'))
           }
        }, true)
    }

    // Load MCWS JSON objects => model, callback(count)
    function loadModelJSON(cmd, model, callback) {
        if (model === undefined) {
            if (Utils.isFunction(callback))
                callback(0)
            return
        }

        // Look for/remove default obj which defines the model data structure
        if (model.count === 1) {
            var defObj = Object.assign({}, model.get(0))
            model.remove(0)
        }

        __exec(hostUrl + cmd, json =>
        {
            try {
               json.forEach(item => {
                    let obj = Object.create(defObj)
                    for (var p in item)
                        obj[Utils.toRoleName(p)] = String(item[p])
                    model.append(obj)
              })
              callback(model.count)
            }
            catch (err) {
               console.log(commandError(err, 'JSON data'))
            }
        }, true)

    }

    // Load a model with mcws objects (MPL)
    function loadModel(cmd, model, callback) {
        if (model === undefined) {
            if (Utils.isFunction(callback))
                callback(0)
            return
        }

        __exec(hostUrl + cmd, xmlStr =>
        {
            var defObj = {}
            if (model.count === 1) {
                defObj = Object.assign({}, model.get(0))
                model.remove(0)
            }

            __createObjectList(xmlStr, obj => model.append(Object.assign(defObj, obj)))

            if (Utils.isFunction(callback))
                callback(model.count)
        })
    }

    // Helper to build obj list for the model
    function __createObjectList(xmlstr, callback) {
        var fldRegExp = /(?:< Name=")(.*?)(?:<\/>)/
        var items = xmlstr
//                .replace(/&quot;/g, '"')
//                .replace(/&#39;/g, "'")
//                .replace(/&lt;/g, '<')
//                .replace(/&gt;/g, '>')
            .replace(/&amp;/g, '&')
            .replace(/Field/g,'')
            .split('<Item>')

        // ignore first item, it's the MPL header info
        for (var i=1, len=items.length; i<len; ++i) {

            var fl = items[i].split('\r\n')
            var fields = {}
            fl.forEach(fldstr =>
            {
                var l = fldRegExp.exec(fldstr)
                if (l !== null) {
                    var o = l.pop().split('">')
                    // Can't convert numbers, same field will vary (string/number)
                    fields[Utils.toRoleName(o[0])] = o[1]
                }
            })
            callback(fields)
        }
    }

    // Run an mcws cmd
    function exec(cmd) {
        __exec(hostUrl + cmd)
    }

}

Couple of helpers:
Code: [Select]
function isFunction(f) {
    return (f && typeof f === 'function')
}
function isObject(o) {
    return (o && typeof o === 'object')
}

A straight JSON reader:
Code: [Select]
function jsonGet(cmdstr, cb) {
    if (!isFunction(cb)) {
        console.warn("Specify callback to get results", cmdstr)
        return
    }

    var xhr = new XMLHttpRequest()

    xhr.onerror = () => {
        console.warn("Unable to connect: ", cmdstr)
    }

    xhr.onreadystatechange = function()
    {
        if (xhr.readyState === XMLHttpRequest.DONE) {

            if (xhr.status === 0) {
                console.warn("Unable to connect: ", cmdstr)
                return
            }
            if (xhr.status !== 200) {
                console.warn('<status: %1:%2>'.arg(xhr.status).arg(xhr.statusText), cmdstr)
                return
            }
            cb(xhr.response)
        }
    }

    xhr.open("GET", cmdstr);
    xhr.responseType = 'json'
    xhr.send();
}
Logged
openSUSE TW/Plasma5 x86_64 | Win10Pro/RX560
S.M.S.L USB-DAC => Transcendent GG Pre (kit) => Transcendent mono OTLs (kit)
(heavily modded) Hammer Dynamics Super-12s (kit)
(optionally) VonSchweikert VR8s

max096

  • MC Beta Team
  • Galactic Citizen
  • *****
  • Posts: 363
Re: Remote Control/Automation issues
« Reply #4 on: May 20, 2021, 08:21:43 am »

It should be fairly doable to write a small one size fits all XML/DOM to workable object mapper (for dynamic scripting languages especially) since their XML responses all look fairly similar. I did this on Android once inflating the XML to a DOM tree and then mapping it. Was very little code, however really slow. So I eventually ended up using XmlPullParser which is a very low-level Android thing. It takes absurd amounts of coding but ended up being 10-20 times faster (I think you would be hard-pressed to beat it with JSON parsers). Since I wanted to parse the entire contents of the library that was important to me (all files and playlists). Back then there was no JSON option for anything. But for smaller things like playback info, this would totally not be an issue.

Configuring an XML parser to spit out proper objects straight away though can be a bit annoying.

What scripting language(s) are you using for your automation stuff?
Logged

RemyJ

  • Regular Member
  • Citizen of the Universe
  • *****
  • Posts: 1249
Re: Remote Control/Automation issues
« Reply #5 on: May 20, 2021, 04:28:36 pm »

Well, I use Python, JavaScript or just plain BASH for scripting depending on the task and yes, there are plenty of solutions out there to get JSON from XML.  Not the point though.  I'd like MC to do it.


Logged
Fedora 40 x86_64 Xfce

max096

  • MC Beta Team
  • Galactic Citizen
  • *****
  • Posts: 363
Re: Remote Control/Automation issues
« Reply #6 on: May 20, 2021, 06:51:05 pm »

Well, I use Python, JavaScript or just plain BASH for scripting depending on the task and yes, there are plenty of solutions out there to get JSON from XML.  Not the point though.  I'd like MC to do it.

Thatīs fair. I donīt know how much effort it is for them to make it return JSON, given that in there are threads from 2017 where people asked for the first ever JSON endpoints and they straight up said 'no, too much work for now'. Im under the impression they did it later to improve performance of jpanel for instance and less so to please  other people using the APIs.

Maybe there is a chance. Hopefully, somebody who actually works for JRiver can step in and awnswer that part. But either way you  are probably better off just getting it done for now, I think.

For good measures here is a little more condenced nodejs version

Code: [Select]
const fetch = require('node-fetch');
const xml2js = require('xml2js')

const ip = '<ip>'
const port = '52199'
const username = '<username>'
const password = '<password>'

async function parseMpl(textContent) {
    const xmlContent = await xml2js.parseStringPromise(textContent)

    let response = {};

    Object.entries(xmlContent.Response.Item).forEach(([_, item]) => {
        const name = item['$']['Name']
        const value = item['_']
        response[name] = value;
    })

    return response;
}

async function getPlaybackInfo() {
    const response = await fetch(`http://${ip}:${port}/MCWS/v1/Playback/Info?Zone=-1`, {
        method: 'GET',
        headers: {
            'Authorization': `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
        }
    })

    if(response.status != 200)
        throw `Status code ${response.status} does not indicate success.`

    const textContent = await response.text()
    return await parseMpl(textContent)
}

async function main() {
    const result = await getPlaybackInfo()
    console.log(JSON.stringify(result))
}

main()

Would be similar in the browser. Fetch just exists, instead of Buffer and toString('base64') you can use atob and instead of xml2js use DOMParser.
Logged

bob

  • Administrator
  • Citizen of the Universe
  • *****
  • Posts: 13874
Re: Remote Control/Automation issues
« Reply #7 on: May 21, 2021, 11:42:54 am »

Besides fiddling with Library Servers, I'm also fiddling with my remote control/automation stuff.   I think these have been mentioned before but it's been a while so I thought I'd mention them again.  None of the /MCC,  /Play, /Pause, /Stop, etc command line options do anything other than give focus to the currently running instance (which is bad by itself).   /MCWS/* options do work but because they also give focus to the running instance, they're not really useful for automation.   

Using the MCWS via the network works fine of course but it does require that the media server be started.  That's fine but could we get an option to get the MCWS output in JSON rather than XML?  JSON is much easier to work with in most scripting languages.
Actually MCC is implemented in the last builds of MC27.
Use the /usr/lib/jriver/Media\ Center\ 27/mc27
stub to do them.
Logged

RemyJ

  • Regular Member
  • Citizen of the Universe
  • *****
  • Posts: 1249
Re: Remote Control/Automation issues
« Reply #8 on: May 22, 2021, 10:40:13 am »

Ah, thanks Bob!   Yeah it does work with the stub.
Logged
Fedora 40 x86_64 Xfce

max096

  • MC Beta Team
  • Galactic Citizen
  • *****
  • Posts: 363
Re: Remote Control/Automation issues
« Reply #9 on: May 23, 2021, 06:00:41 pm »

Actually MCC is implemented in the last builds of MC27.
Use the /usr/lib/jriver/Media\ Center\ 27/mc27
stub to do them.

Thatīs pretty cool! Didnīt know that was a thing since MC27 somewhen. Kinda overslept that part. MCWS is great, but it kind of falls apart when you want to build automation into things where you cannot (yet) know what the username and password is gonna be for MC.
Logged
Pages: [1]   Go Up