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
How to Get the IP of a Sonos Device
1. Go to the Help Menu
2. Get IP Address from Summary Information
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | < 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 > |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | 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. 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( "<item" , j); while (i >= 0) { // Loop over all items, where each item is a track on the queue. j = response.indexOf( "</item>" , i); String track = response.substring(i + 8, j); String trackNo, artist, title, unc; trackNo = extract(track, " id="Q:" , """ ); trackNo = trackNo.substring(trackNo.indexOf('/ ') + 1); unc = URLDecoder.decode(extract(track, ">x-file-cifs:", "<").replace(' / ', ' \\ ').replaceAll("%20", " "), "UTF-8"); artist = extract(track, "<dc:creator>", "</dc:creator>"); title = extract(track, "<dc:title>", "</dc:title>"); txtFile.append(decode(trackNo + ". " + artist + ": " + title) + "\r\n"); m3uFile.append(decode(unc) + "\r\n"); i = response.indexOf("<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 & to &, then uses Apache * Commons Lang to decode the standard entities and then manually decodes a non- * standard entity ('). * @param s Text to be decoded. * @return Text with HTML character entities decoded. */ private String decode(String s) { // Convert & to &, ' to ' and let Apache Commons Lang about the rest. return StringEscapeUtils.unescapeHtml3(s.replaceAll( "&" , "&" )).replaceAll( "'" , "'" ); } /** * 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( "&" , "&" ); out = out.replaceAll( "'" , "'" ); out = out.replaceAll( """ , "\"" ); out = out.replaceAll( "<" , "<" ); out = out.replaceAll( ">" , ">" ); out = out.replaceAll( " " , " " ); 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." ); } } |
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
ReplyDeleteso 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?
Everything that comes in Java has its underlying foundations to the protest arranged programming. java programming
ReplyDelete