MediaTomb dspl support

dspl format

The dspl (digitalSTROM playlist) file is simply the JSON-file that can be fetched from the dSS at http://[dSS]/json/apartment/getStructure.

Steps to enable dspl support

Several files have to be modified to enable dspl support in MediaTomb. I recommend to make a copy of each file before modifying it because this makes it very easy to enable and disable dspl support later on. The necessary modifications are described file by file. Copying the file will not be mentioned for each file, just do it if you need the possibility to enable/disable dspl-support. All the JavaScript files are located in the MediaTomb script directory, which is probably /usr/local/share/mediatomb/js/ (depending on the installation). The XML-files are by default located in ~/.mediatomb/.

common.js

The function getPlayListType in the common.js file in the Mediatomb script directory has to be replaced by

function getPlaylistType(mimetype)
{
    if (mimetype == 'audio/x-mpegurl')
        return 'm3u';
    if (mimetype == 'audio/x-scpls')
        return 'pls';
    if (mimetype == 'playlist/dspl')
        return 'dspl';
    return '';
}

playlists.js

At the end of the playlists.js in the MediaTomb script directory (which should be the closing curly bracket of the else if (type == 'pls') branch) the following else-if-branch has to be added. Remember to replace all file paths with the paths of the corresponding files on your machine. The code for the call_scene_item_by_state.py, upnp_call_device and upnp_call_scene scripts (the latter two are called from call_scene_item_by_state.py) can be found at the end of this page.

// the following else if branch is added to process dspl playlists
else if (type == 'dspl')
{
    // read whole json file
    var json_content = "";
    var line = readln();
    do
    {
    json_content = json_content + line;
        line = readln();
    }
    while (line);

    // evaluate content of json file
    apartment = eval('(' + json_content + ')').apartment;

    // traverse structure and add appropriate active items
    while(apartment.zones.length > 0)
    {
        zone = apartment.zones.shift();

    // zone 0 is not displayed...
    if(zone.id)
        {

            // show devices only if flag is set
            if(SHOW_DEVICES)
            {
                // add every device in zone
                while(zone.devices.length > 0)
                {
                    device = zone.devices.shift();

                    addPlaylistItemOfDevice(zone.name, device);
                }
            }

            addScenes(zone.id, zone.name);
    }
    }    
}

var SHOW_DEVICES = false;

var prefix = "Automatisch gefundene Zonen" 

function audioContainerChain(arr)
{
    var base = new Array("Wohnung - Musik");
    return createContainerChain(base.concat(arr));
}

function imageContainerChain(arr)
{
    var base = new Array("Wohnung - Photos");
    return createContainerChain(base.concat(arr));
}

var devPLOrder = 1;

function addPlaylistItemOfDevice(zone, device)
{
    var item = new Object();

    print("Adding device ``" + device.name + "'' in zone ``" + zone + "''");

    // set general information of audio _and_ image items
    item.objectType = OBJECT_TYPE_ACTIVE_ITEM;
    if (device.name == '')
        item.title = device.id;
    else
        item.title = device.name;

    item.description = device.name;
    item.action = "/home/demo/Desktop/call_scene_item_by_state.py";
    item.state = "class=device;device="+device.id + ";";

    // add audio item to database
    audioItem = eval(uneval(item)); // clone object

    audioItem.location = "/home/demo/Desktop/send.mp3";
    audioItem.mimetype = 'audio/mpeg';
    audioItem.upnpclass = UPNP_CLASS_ITEM_MUSIC_TRACK;

    audioItemOn = eval(uneval(audioItem));
    audioItemOn.title += " Ein";
    audioItemOn.description = audioItemOn.title;
    audioItemOn.state += "action=on;";
    audioItemOn.playlistOrder = devPLOrder++;

    addCdsObject(audioItemOn,
                 audioContainerChain(new Array(zone, "Devices",
                                               audioItem.title,
                                               "Ein")),
                 UPNP_CLASS_PLAYLIST_CONTAINER);

    audioItemOff = eval(uneval(audioItem));
    audioItemOff.title += " Aus";
    audioItemOff.description = audioItemOff.title;
    audioItemOff.state += "action=off";
    audioItemOff.playlistOrder = devPLOrder++;

    addCdsObject(audioItemOff,
                 audioContainerChain(new Array(zone, "Devices",
                                               audioItem.title,
                                               "Aus")),
                 UPNP_CLASS_PLAYLIST_CONTAINER);

    // add image item to database
    imageItem = eval(uneval(item));

    imageItem.mimetype = 'image/jpeg';
    imageItem.upnpclass = "object.item.imageItem";

    imageItemOn = eval(uneval(imageItem));
    imageItemOn.title += " Ein";
    imageItemOn.description = imageItemOn.title;
    imageItemOn.state += "action=on;" 
    imageItemOn.location = "/home/demo/Desktop/light_10.jpeg";
    imageItemOn.playlistOrder = devPLOrder++;

    addCdsObject(imageItemOn,
                 imageContainerChain(new Array(zone, "Devices",
                                               imageItem.title,
                                               "Ein")),
                 UPNP_CLASS_PLAYLIST_CONTAINER);

    imageItemOff = eval(uneval(imageItem));
    imageItemOff.title += " Aus";
    imageItemOff.description = imageItemOff.title;
    imageItemOff.state += "action=off;" 
    imageItemOff.location = "/home/demo/Desktop/light_01.jpeg";
    imageItemOff.playlistOrder = devPLOrder++;

    addCdsObject(imageItemOff,
                 imageContainerChain(new Array(zone, "Devices",
                                               imageItemOff.title,
                                               "Aus")),
                 UPNP_CLASS_PLAYLIST_CONTAINER);
}

// creator function for Scene object
function Scene(id, name)
{
    this.id = id;
    this.name = name;
}

// add scenes 1 to 4 and scene off for a zone to audio and image container
function addScenes(zoneid, zonename) {
    var item = new Object();

    // scene id -> scene name mapping
    if(zoneid == 313) // Wohnen
    {
        scenes = new Array(new Scene(5, "Essen"),
                           new Scene(17, "Fernsehen"),
                           new Scene(18, "Lesen"),
                           new Scene(19, "Putzen"),
                           new Scene(13, "Alles aus"));
    }
    else if(zoneid == 357) // Garage
    {
        scenes = new Array(new Scene(5, "Scene 1"),
                           new Scene(17, "Scene 2"),
                           new Scene(18, "Scene 3"),
                           new Scene(19, "Scene 4"),
                           new Scene(13, "Alles aus"));
    }
    else if(zoneid == 324) // Schlafen
    {
        scenes = new Array(new Scene(5, "Scene 1"),
                           new Scene(17, "Scene 2"),
                           new Scene(18, "Scene 3"),
                           new Scene(19, "Scene 4"),
                           new Scene(13, "Alles aus"));
    }
    else if(zoneid == 1) // Bad
    {
        scenes = new Array(new Scene(5, "Scene 1"),
                           new Scene(17, "Scene 2"),
                           new Scene(18, "Scene 3"),
                           new Scene(19, "Scene 4"),
                           new Scene(13, "Alles aus"));
    }
    else
    {
        scenes = new Array(new Scene(5, "Scene 1"),
                           new Scene(17, "Scene 2"),
                           new Scene(18, "Scene 3"),
                           new Scene(19, "Scene 4"),
                           new Scene(13, "Alles aus"));
    }

    print("Adding Scenes in zone ``" + zonename + "''");

    // set general information of audio _and_ image items
    item.objectType = OBJECT_TYPE_ACTIVE_ITEM;
    item.action = "/home/demo/Desktop/call_scene_item_by_state.py";

    index = 1;
    while(scenes.length > 0)
    {
        scene = scenes.shift();

        // set scene dependant properties
        item.title = scene.name;
        item.description = scene.name;
        item.state = "class=scene;zone="+zoneid+";scene="+scene.id;
        item.playlistOrder = index++;

        // create audio item
        audioItem = eval(uneval(item));
        audioItem.location = "/home/demo/Desktop/send.mp3";
        audioItem.mimetype = 'audio/mpeg';
        audioItem.upnpclass = UPNP_CLASS_ITEM_MUSIC_TRACK;
        addCdsObject(audioItem,
                     audioContainerChain(new Array(zonename, audioItem.title)),
                     UPNP_CLASS_PLAYLIST_CONTAINER);

        // create image item
        imageItem = eval(uneval(item));
        imageItem.location = "/home/demo/Desktop/sc" + scene.id + ".jpg";
        imageItem.mimetype = 'image/jpeg';
        imageItem.upnpclass = "object.item.imageItem";
        addCdsObject(imageItem,
                     imageContainerChain(new Array(zonename, imageItem.title)),
                     UPNP_CLASS_PLAYLIST_CONTAINER);
    }
}

config.xml

The following tags have to be added to the config.xml file:

  • <map from="dspl" to="playlist/dspl"/>
    to the
    <extension-mimetype ...>
    tag
  • <treat mimetype="playlist/dspl" as="playlist"/>
    to the
    <mimetype-contenttype>
    tag

In case you copied the script files, the path to the new common.js file has to be set in the

<common-script>
tag and the path to the new playlists.js file has to be set in the
<playlist-script>
tag.

If you copied all files before modifying them, enabling and disabling dspl-support is as easy as executing MediaTomb with the corresponding config file (mediatomb -c ...).

Files

call_scene_item_by_state.py

#!/usr/bin/env python
# $Id: demo_toggle.py 1294 2007-05-13 16:28:24Z lww $
import sys
from xml.dom.minidom import *
import os

class Item:
    def __init__(self, input):
        self.xml = parseString(input)

    def __getNodeData(self, data):
        try:
            title = self.xml.getElementsByTagName(data)[0]
            if (len(title.childNodes) > 0):
                if (title.childNodes[0].nodeType == title.childNodes[0].TEXT_NODE):
                    return title.childNodes[0].data

            return ''
        except:
            return ''

    def __getNode(self, data):
        try:
            title = self.xml.getElementsByTagName(data)[0]
            if (len(title.childNodes) > 0):
                if (title.childNodes[0].nodeType == title.childNodes[0].TEXT_NODE):
                    return title.childNodes[0]
            else:
                text = self.xml.createTextNode("")
                title.appendChild(text)
                return title.childNodes[0]

        except:
            return None

    def getTitle(self):
        return self.__getNodeData("dc:title")

    def setTitle(self, data):
        node = self.__getNode("dc:title")
        node.data = data 

    def getClass(self):
        return self.__getNodeData("upnp:class")

    def setClass(self, data):
        node = self.__getNode("upnp:class")
        node.data = data 

    def getAction(self):
        return self.__getNodeData("action")

    def setAction(self, data):
        node = self.__getNode("action")
        node.data = data 

    def getState(self):
        return self.__getNodeData("state")

    def setState(self, data):
        node = self.__getNode("state")
        node.data = data 

    def getLocation(self):
        return self.__getNodeData("location")

    def setLocation(self, data):
        node = self.__getNode("location")
        node.data = data 

    def getMimeType(self):
        return self.__getNodeData("mime-type")

    def setMimeType(self, data):
        node = self.__getNode("mime-type")
        node.data = data 

    def render(self):
        return self.xml.toxml()

    def getDescription(self):
        return self.__getNodeData("dc:description")

    def setDescription(self, data):
        node = self.__getNode("dc:description")
        node.data = data 

item = Item(sys.stdin.read())

#
# The following part of the file reads parameters from the state of the item
# and carries out an appropriate upnp call using the upnp_call_device script.
#
# Author: Sebastian Kohler (sebastian.kohler@aizo.com)
#

# split parameter list string into a list of parameters, i.e. a list name=value pairs
parameterList = item.getState().split(';');

# split each parameter into name and value
param = {}
for p in parameterList:
    tmp = p.split('=', 1);
    if len(tmp) == 2:
        param[tmp[0]] = tmp[1]

# read parameters and finally carry out appropriate upnp call
if 'class' in param:
    if param['class'] == 'device':
        if 'action' in param:
            if param['action'] == 'on':
                on = 1;
            else:
                on = 0;
            os.system("/home/demo/Desktop/upnp_call_device %s %i" % (param['device'], on))
        else:
            print "no action found" 

    elif param['class'] == 'scene':
        if not 'zone' in param:
            print "Error: no zone found" 
        elif not 'scene' in param:
            print "Error: no scene found" 
        else:
            zone =  int(param['zone'])
            scene = int(param['scene'])
# Scene 13 (Alles aus) is blocked if no other scene has been called inbetween.
# This is necessary because some devices send after rebooting the last sent request, which
# might scene 13, which would lead to a loop of shutting down and rebooting...
            lockfile = "/home/demo/Desktop/zone"+str(zone)+".lock" 
            if scene == 13:
                if os.path.exists(lockfile):
                    call = False;
                else:
                    FILE = open(lockfile,"w")
                    FILE.write("DO NOT REMOVE THIS FILE! This file is a lock file used by MediaTomb to avoid some issues with calling scenes on the dSS...")
                    FILE.close();
                    call = True;
            else:
                if os.path.exists(lockfile):
                     os.remove(lockfile)
                call = True;
            if call:
                os.system("/home/demo/Desktop/upnp_call_scene %i %i" % (zone, scene))
    else:
        print "Error: unknown class" + param['class']
else:
    print "Error: no class found" 

sys.stdout.write(item.render())

upnp_call_device

#!/bin/bash
#
# ask the dss to turn a device on/off
#
# Sebastian Kohler, sebastian.kohler@aizo.com

DSS_SERVER=localhost:8080
BASE_URL="http://$DSS_SERVER/json/device/" 

if [ -z "$2" ]; then
    echo "Usage: $(basename $0) <decive id> [0|1]" 
    exit 1
fi

set -x

if [ $2 -eq 0 ]; then
    wget -O - "${BASE_URL}turnOff?dsid=$1" &> /dev/null
else
    wget -O - "${BASE_URL}turnOn?dsid=$1" &> /dev/null
fi

upnp_call_scene

#!/bin/bash
#
# ask the dss to call a scene
#
# Johannes Winkelmann, johannes.winkelmann@aizo.com

DSS_SERVER=localhost:8080
ZONE_ID=0
BASE_URL="http://$DSS_SERVER/json/zone/callScene" 

if [ -z "$2" ]; then
    echo "Usage: $(basename $0) <zone id> <scene id>" 
    exit 1
fi

set -x
wget -O - "${BASE_URL}?id=$1&sceneNr=$2&groupID=1" &> /dev/null