oddbit/sonosjs

View on GitHub
src/models/state.js

Summary

Maintainability
B
4 hrs
Test Coverage
define(function (require) {
        "use strict";

        var xml = require("utils/xml");
        var media = require("models/media");


        /**
         * Identifiers for state information objects.
         *
         * @readonly
         * @enum {number|string}
         * @typedef {object} StateInformationType
         */
        var stateInformationType = {
            LAST_CHANGE: "%%E:MODEL_STATE_LAST_CHANGE%%",
            GROUP_VOLUME: "%%E:MODEL_STATE_GROUP_VOLUME%%"
        };

        /**
         * Play state for the device.
         *
         * @readonly
         * @enum {string}
         * @typedef {object} PlayState
         */
        var playStateType = {
            STOPPED: "STOPPED",
            PAUSED: "PAUSED",
            PLAYING: "PLAYING"
        };

        /**
         * The play mode tells how the next media item in the list will be selected. Either top down (ORDERED) or
         * randomly (SHUFFLE).
         *
         * @readonly
         * @enum {string}
         * @typedef {object} PlayMode
         */
        var playModeType = {
            ORDERED: "ORDERED",
            SHUFFLE: "SHUFFLE"
        };

        /**
         * Repeat mode for the play queue.
         *
         * @readonly
         * @enum {string}
         * @typedef {object} RepeatMode
         */
        var repeatModeType = {
            OFF: "NO_REPEAT",
            ALL: "REPEAT_ALL"
        };

        /**
         * Last change information contains the last known information about a device and what was playing.
         *
         * The following play modes are defined by SONOS and are expected values for the parameter
         *
         *   - NORMAL           - No repeat, no shuffle
         *   - REPEAT_ALL       - Repeat the whole playlist, no shuffle
         *   - SHUFFLE          - Repeat the whole playlist and shuffle... yes, this is hard to accept
         *   - SHUFFLE_NOREPEAT - No repeat, but do shuffle
         *
         * @param {object}          opts                    Object initializing options
         * @param {string}          opts.playState          "STOPPED", "PAUSED_PLAYBACK" or "PLAYING"
         * @param {string}          opts.playMode           "NORMAL", "REPEAT_ALL", "SHUFFLE", "SHUFFLE_NOREPEAT"
         * @param {object}          opts.mediaInfo          mediaInfo object
         * @param {number|string}   opts.numberOfTracks     Total number of tracks in the play queue
         * @returns {object} Device state information
         */
        function lastChange(opts) {
            opts = opts || {};

            var that = {};

            var playState = opts.playState;
            var playMode = opts.playMode;

            that.model = stateInformationType.LAST_CHANGE;
            that.mediaInfo = opts.mediaInfo;
            that.numberOfTracks = Number(opts.numberOfTracks);
            that.playState = playStateType.STOPPED;
            that.repeatMode = repeatModeType.OFF;
            that.playMode = playModeType.ORDERED;

            (function init() {
                switch (playState) {
                case "PLAYING":
                    that.playState = playStateType.PLAYING;
                    break;
                case "PAUSED_PLAYBACK":
                    that.playState = playStateType.PAUSED;
                    break;
                case "STOPPED":
                    that.playState = playStateType.STOPPED;
                    break;
                default:
                    console.warn("Unknown play state: ", playState);
                    that.playState = playStateType.STOPPED;
                    break;
                }

                switch (playMode) {
                case "NORMAL":
                    that.repeatMode = repeatModeType.OFF;
                    that.playMode = playModeType.ORDERED;
                    break;

                case "REPEAT_ALL":
                    that.repeatMode = repeatModeType.ALL;
                    that.playMode = playModeType.ORDERED;
                    break;

                case "SHUFFLE":
                    that.repeatMode = repeatModeType.ALL;
                    that.playMode = playModeType.SHUFFLE;
                    break;

                case "SHUFFLE_NOREPEAT":
                    that.repeatMode = repeatModeType.OFF;
                    that.playMode = playModeType.SHUFFLE;
                    break;

                default:
                    console.warn("Unknown play mode: ", playMode);
                    that.repeatMode = repeatModeType.OFF;
                    that.playMode = playModeType.ORDERED;
                    break;
                }
            }());

            return that;
        }

        /**
         * Factory method for lastChange from a XML string.
         *
         * @param {string}      xmlString
         * @param {function}    callback    Method to call when finished
         */
        lastChange.fromXml = function (xmlString, callback) {
            // Everything below the LastChange tag is URL encoded and the meta data is encoded twice
            xmlString = xml.decode(xmlString);
            var xmlParser = xml.parser();

            // Local helper function
            function getValueAttribute(xmlNode) {
                if (!xmlNode) {
                    return null;
                }

                return xmlNode.attributes && xmlNode.attributes.val;
            }

            xmlParser.parse(xmlString, function () {
                var queryBase = "/propertyset/property/LastChange/Event/InstanceID/";

                var mediaInfo = media.info({
                    playQueueNumber: getValueAttribute(xmlParser.query(queryBase + "CurrentTrack")[0]),
                    duration: getValueAttribute(xmlParser.query(queryBase + "CurrentTrackDuration")[0]),
                    uri: getValueAttribute(xmlParser.query(queryBase + "CurrentTrackURI")[0]),
                    currentTime: getValueAttribute(xmlParser.query(queryBase + "CurrentRelTime")[0]) // Just guessing from naming convention
                });


                // Call this method to finish the last data structures when the underlying structures are done
                var metaDataXml = getValueAttribute(xmlParser.query(queryBase + "CurrentTrackMetaData")[0]);

                media.metaData.fromXml(mediaInfo, metaDataXml, function (metaData) {
                    mediaInfo.metaData = metaData;
                    var deviceStateOpts = {
                        playState: getValueAttribute(xmlParser.query(queryBase + "TransportState")[0]),
                        playMode: getValueAttribute(xmlParser.query(queryBase + "CurrentPlayMode")[0]),
                        numberOfTracks: getValueAttribute(xmlParser.query(queryBase + "NumberOfTracks")[0]),
                        mediaInfo: mediaInfo
                    };

                    // TODO: Parse the NextTrack<...> information and create a nextMediaInfo property

                    callback(lastChange(deviceStateOpts));
                });
            });
        };


        /**
         * Information about the volume level of a media group (also referred to as "room" by SONOS)
         *
         * @param {object}  opts                    Object initializing options
         * @param {number}  opts.volume
         * @param {boolean} opts.canChangeVolume
         * @param {boolean} opts.isMuted
         *
         * @returns {object} Group volume object
         */
        function groupVolume(opts) {
            opts = opts || {};

            var that = {};

            that.model = stateInformationType.GROUP_VOLUME;
            that.volume = Number(opts.volume || 0);
            that.canChangeVolume = Number(opts.canChangeVolume || 0) === 1;
            that.isMuted = Number(opts.mute || 0) === 1;

            return that;
        }

        /**
         * Factory method for groupVolume from a XML string.
         *
         * @param {string}      xmlString
         * @param {function}    callback    Method to call when finished
         */
        groupVolume.fromXml = function (xmlString, callback) {
            var xmlParser = xml.parser();
            xmlParser.parse(xmlString, function () {

                var groupVolumeOpts = media.info({
                    volume: xmlParser.query("/propertyset/property/GroupVolume")[0].text,
                    mute: xmlParser.query("/propertyset/property/GroupMute")[0].text,
                    canChangeVolume: xmlParser.query("/propertyset/property/GroupVolumeChangeable")[0].text
                });

                callback(groupVolume(groupVolumeOpts));
            });
        };

        return {
            informationType: stateInformationType,
            playState: playStateType,
            playMode: playModeType,
            repeatMode: repeatModeType,
            lastChange: lastChange,
            groupVolume: groupVolume
        };
    }
);