Sunday, February 5, 2012

Java Program to Extract What’s in a Sonos Queue

Sonos Controller for Windows
This Java program takes what’s in the Sonos queue and outputs it to a list in a text file and an .m3u file. The origin of this Java program to extract the Sonos queue started with some work we did for the post A Simple Sonos JavaScript Application. A reader (see the comments) contributed a Java program to get the current queue and save its contents to a file. After that effort, we worked extensively on WPF Application to Save and Import Sonos Playlists and learned a few tricks. What we learned there, in particular, getting results recursively, we used to revise the initial Java queue extraction program. This post presents the revised program. It only gets what’s in the queue, so if you want to use this to save playlists you would first need to clear the queue, add a playlist to the queue, run this program (with the correct parameters), and repeat for as many playlists you want to save.

If you don’t know a lot about Java and running programs on your computer (in this case we show it for Windows), go see this post, Java, Apache Ant and Hello World.

You can run the code in several different ways, with Eclipse or just at the command line (just run “ant”) are probably the two most common ways. The program requires three inputs the IP address of a master device, the name of a folder to put the output files, and the name of the output file. Both a text version and an .m3u version of the queue are output.

To get the IP address you can use the Help menu in the Sonos Controller and it shows a summary of the devices it sees. The IP address can be found in the summary. Be warned though, the first item in the list is not necessarily the master and running this program without specifying the IP address of the master will not return any results. That said, if you are having trouble, just reduce a zone to one device and then it will be the master.

The Ant build task and the program are shown below.

Sonos Queue Extractor Running in Eclipse and at the Command Line with Ant
Sonos Queue Extractor - Java, EclipseSonos Queue Extractor - Java, ANT Task

How to Get the IP of a Sonos Device
1. Go to the Help Menu

Sonos Controller Windows
2. Get IP Address from Summary Information
Sonos Controller Windows

The Ant Task
<project name="SonosQueueExtractor " basedir="." default="main">
<property name="src.dir" value="src"/>
<property name="build.dir" value="build"/>
<property name="classes.dir" value="${build.dir}/classes"/>
<property name="jar.dir" value="${build.dir}/jar"/>
<property name="lib.dir" value="lib"/>
<property name="main-class" value="Travelmarx.SonosQueueExtractor"/>

<path id="classpath">
<fileset dir="${lib.dir}" includes="**/*.jar"/>
</path>
<target name="clean">
<delete dir="${build.dir}"/>
</target>
<target name="compile">
<mkdir dir="${classes.dir}"/>
<javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath" includeantruntime="false"/>
</target>
<target name="jar" depends="compile">
<mkdir dir="${jar.dir}"/>
<jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${classes.dir}">
<manifest>
<attribute name="Main-Class" value="${main-class}"/>
</manifest>
</jar>
</target>
<target name="run" depends="jar">
<java classname="${main-class}">
<classpath>
<path refid="classpath"/>
<path location="${jar.dir}/${ant.project.name}.jar"/>
</classpath>
<arg value="192.168.2.155"/>
<arg value="c:\public"/>
<arg value="queue"/>
</java>
</target>
<target name="clean-build" depends="clean,jar"/>
<target name="main" depends="clean,run"/>
</project>
The Java Program: Sonos Queue Extractor
package Travelmarx;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

import java.io.InputStreamReader;
import java.io.OutputStreamWriter;

import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLDecoder;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.lang3.StringEscapeUtils;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;


/**
* Class responsible for extracting current Sonos queue to a playlist and text file.
*/

public class SonosQueueExtractor {

public static Integer maxPageResults = 50;
public static String queueName = "Q:0"; // the main queue, can't be used currently with playlist like SQ:14
public static OutputStreamWriter txtFile;
public static OutputStreamWriter m3uFile;
public static String ipAddress;

public SonosQueueExtractor()
throws MalformedURLException, ProtocolException, IOException, SAXException, ParserConfigurationException {

queryQueue(0);
}

private void queryQueue(Integer pageIndex) throws IOException, ParserConfigurationException, SAXException {
// Build HTTP request with SOAP envelope asking for details about the
// current queue.
URL url = new URL("http://" + ipAddress + ":1400/MediaServer/ContentDirectory/Control");
HttpURLConnection request = (HttpURLConnection)url.openConnection();

request.setRequestMethod("POST");
request.addRequestProperty("SOAPACTION", "\"urn:schemas-upnp-org:service:ContentDirectory:1#Browse\"");
request.setDoOutput(true);
request.setReadTimeout(2000);

Integer startPageResults = maxPageResults*pageIndex;
Integer numTotalMatches = 0;

request.connect();

OutputStreamWriter input = new OutputStreamWriter(request.getOutputStream());
input.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n");
input.write("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">\r\n");
input.write(" <s:Body>\r\n");
input.write(" <u:Browse xmlns:u=\"urn:schemas-upnp-org:service:ContentDirectory:1\">\r\n");
input.write(" <ObjectID>" + queueName + "</ObjectID>\r\n");
input.write(" <BrowseFlag>BrowseDirectChildren</BrowseFlag>\r\n");
input.write(" <Filter>upnp:artist,dc:title</Filter>\r\n");
input.write(" <StartingIndex>" + startPageResults.toString() + "</StartingIndex>\r\n");
input.write(" <RequestedCount>" + maxPageResults.toString() + "</RequestedCount>\r\n");
input.write(" <SortCriteria></SortCriteria>\r\n");
input.write(" </u:Browse>\r\n");
input.write(" </s:Body>\r\n");
input.write("</s:Envelope>\r\n");
input.flush();

// Read entire HTTP response, which is assumed to be in UTF-8.
BufferedReader output = new BufferedReader(new InputStreamReader(request.getInputStream(), "UTF-8"));
String oneResponse = new String();
String line;

while ((line = output.readLine()) != null) {
oneResponse += line + "\r\n";
}

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true);
DocumentBuilder db = dbf.newDocumentBuilder();
String root = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>" + xmlDecode(oneResponse);
Document doc = db.parse(new ByteArrayInputStream(root.getBytes()));
NodeList nodeList = doc.getElementsByTagName("TotalMatches");
numTotalMatches = Integer.parseInt(nodeList.item(0).getTextContent());
Integer nextPageResult = ((startPageResults + maxPageResults) < numTotalMatches) ? startPageResults + maxPageResults : numTotalMatches;
System.out.println("Writing " + startPageResults.toString() + " to " + nextPageResult + " out of "+ numTotalMatches + " matches.");

if (numTotalMatches > maxPageResults*(pageIndex + 1)) {
// Get more items, recursively
pageIndex += 1;
writeToFiles(oneResponse);
queryQueue(pageIndex);
}
else {
writeToFiles(oneResponse);
}

request.disconnect();

}

private void writeToFiles(String response) throws IOException {

int i = 0, j = 0;
i = response.indexOf("&lt;item", j);
while (i >= 0) {
// Loop over all items, where each item is a track on the queue.
j = response.indexOf("&lt;/item&gt;", i);
String track = response.substring(i + 8, j);
String trackNo, artist, title, unc;

trackNo = extract(track, " id=&quot;Q:", "&quot;");
trackNo = trackNo.substring(trackNo.indexOf('/') + 1);
unc = URLDecoder.decode(extract(track, "&gt;x-file-cifs:", "&lt;").replace('/', '\\').replaceAll("%20", " "), "UTF-8");
artist = extract(track, "&lt;dc:creator&gt;", "&lt;/dc:creator&gt;");
title = extract(track, "&lt;dc:title&gt;", "&lt;/dc:title&gt;");

txtFile.append(decode(trackNo + ". " + artist + ": " + title) + "\r\n");
m3uFile.append(decode(unc) + "\r\n");

i = response.indexOf("&lt;item", j);
}
}
/**
* Extracts text surrounded by markers from given string.
* @param s String to extract text from.
* @param start Start marker.
* @param stop Stop marker.
* @return Extracted text found between start and stop markers, markers
* excluded.
*/

private String extract(String s, String start, String stop) {
int i = s.indexOf(start) + start.length();

return s.substring(i, s.indexOf(stop, i));
}

/**
* Decodes HTML character entities. First changes &amp; to &, then uses Apache
* Commons Lang to decode the standard entities and then manually decodes a non-
* standard entity (&apos;).
* @param s Text to be decoded.
* @return Text with HTML character entities decoded.
*/

private String decode(String s) {
// Convert &amp; to &, &apos; to ' and let Apache Commons Lang about the rest.
return StringEscapeUtils.unescapeHtml3(s.replaceAll("&amp;", "&")).replaceAll("&apos;", "'");
}

/**
* Decodes XML character entities. Uses the Apache
* Commons Lang to decode the standard entity.
* @param s Text to be decoded.
* @return Text with XML character entities decoded.
*/
private String xmlDecode(String s) {
String out = s.replaceAll("&amp;", "&");
out = out.replaceAll("&apos;", "'");
out = out.replaceAll("&quot;", "\"");
out = out.replaceAll("&lt;", "<");
out = out.replaceAll("&gt;", ">");
out = out.replaceAll("&nbsp;", " ");
return out;
}

/**
* Extracts current Sonos queue and saves track information to a playlist file
* and a text file. The playlist file is saved in .m3u format and the text file
* is a plain text file with each line in the format
* <track_no>. <artist>: <title>
* Both files are in ISO8859-1 format.
* @param args 0: Sonos master IP address.
* 1: Export file path.
* 2: Playlist name.
* @throws MalformedURLException
* @throws ProtocolException
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
*/

public static void main(String[] args)
throws MalformedURLException, ProtocolException, IOException, SAXException, ParserConfigurationException {
if (args.length < 3) {
System.err.println("Usage: SonosQueueExtractor sonos_master_ip_address export_file_path playlist_name");
System.exit(0);
}

ipAddress = args[0];
// Open output files, both in ISO8859-1 encoding.
txtFile = new OutputStreamWriter(new FileOutputStream(new File(args[1] + "/" + args[2] + ".txt")), "8859_1");
m3uFile = new OutputStreamWriter(new FileOutputStream(new File(args[1] + "/" + args[2] + ".m3u")), "8859_1");
new SonosQueueExtractor ();

txtFile.close();
m3uFile.close();
System.out.println("Check " + args[1] + " for output files.");

}
}

2 comments:

  1. hey so i'm traying to develop a program to work with the sonos system that will act one what ever the sonos system is playing

    so my question is there a way to get information on what kinda off track is being played like line in radio spotify ore a diffrent source?

    ReplyDelete
  2. Everything that comes in Java has its underlying foundations to the protest arranged programming. java programming

    ReplyDelete