commit 624e99c2d5175af9b486a8d0c51b78f0306f6de4
Author: Brian Swetland <swetland@frotz.net>
Date: Sun, 24 Jul 2011 10:49:19 -0700
initial checkin -- much to be done
Signed-off-by: Brian Swetland <swetland@frotz.net>
Diffstat:
8 files changed, 710 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,3 @@
+*.class
+*.jar
+.gitignore
diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: net.frotz.sonos.app
+Created-By: vi
diff --git a/Makefile b/Makefile
@@ -0,0 +1,12 @@
+
+SRCS := $(wildcard net/frotz/sonos/*.java)
+
+all: sonos.jar
+
+sonos.jar: $(SRCS)
+ javac $(SRCS)
+ rm -rf sonos.jar
+ zip -qr sonos.jar META-INF net -x \*.java
+
+clean::
+ rm -rf net/frotz/sonos/*.class sonos.jar
diff --git a/net/frotz/sonos/SoapRPC.java b/net/frotz/sonos/SoapRPC.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2011 Brian Swetland
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.frotz.sonos;
+
+import java.net.InetAddress;
+import java.net.Socket;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+class SoapRPC {
+ public boolean trace;
+ InetAddress addr;
+ int port;
+ ByteBuffer reply;
+ XML xml;
+
+ public SoapRPC(byte[] host, int port) {
+ try {
+ addr = InetAddress.getByAddress(host);
+ } catch (Exception x) {
+ }
+ this.port = port;
+
+ reply = ByteBuffer.wrap(new byte[32768]);
+ xml = new XML(32768);
+ }
+ void call(byte[] data) {
+ try {
+ reply.clear();
+ byte[] buf = reply.array();
+ Socket s = new Socket(addr,port);
+ OutputStream out = s.getOutputStream();
+ InputStream in = s.getInputStream();
+ out.write(data);
+ int off = 0;
+ for (;;) {
+ int r = in.read(buf, off, buf.length - off);
+ if (r <= 0) break;
+ off += r;
+ }
+ reply.limit(off);
+ s.close();
+ if (trace) {
+ System.out.println("--------- reply -----------");
+ System.out.println(new String(buf, 0, off));
+ }
+ } catch (Exception x) {
+ System.out.println("OOPS: " + x.getMessage());
+ x.printStackTrace();
+ }
+ }
+ public XML call(Endpoint ept, String method, String payload) {
+ StringBuilder msg = new StringBuilder();
+ msg.append("<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\" s:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\"><s:Body>");
+ msg.append("<u:");
+ msg.append(method);
+ msg.append(" xmlns:u=\"urn:schemas-upnp-org:service:");
+ msg.append(ept.service);
+ msg.append("\">");
+ msg.append(payload);
+ msg.append("</u:");
+ msg.append(method);
+ msg.append("></s:Body></s:Envelope>\n");
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("POST ");
+ sb.append(ept.path);
+ sb.append(" HTTP/1.0\r\n");
+ sb.append("CONNECTION: close\r\n");
+ sb.append("Content-Type: text/xml; charset=\"utf-8\"\r\n");
+ sb.append("Content-Length: ");
+ sb.append(msg.length());
+ sb.append("\r\n");
+ sb.append("SOAPACTION: \"urn:schemas-upnp-org:service:");
+ sb.append(ept.service);
+ sb.append("#");
+ sb.append(method);
+ sb.append("\"\r\n\r\n");
+ sb.append(msg);
+
+ if (trace) {
+ System.out.println("--------- message -----------");
+ System.out.println(sb);
+ }
+
+ byte[] data = sb.toString().getBytes();
+
+ call(data);
+
+ xml.init(reply);
+ try {
+ xml.open("s:Envelope");
+ xml.open("s:Body");
+ return xml;
+ } catch (XML.Oops x) {
+ return null;
+ }
+ }
+ public static class Endpoint {
+ String service,path;
+ public Endpoint(String service, String path) {
+ this.service = service;
+ this.path = path;
+ }
+ }
+}
diff --git a/net/frotz/sonos/Sonos.java b/net/frotz/sonos/Sonos.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2011 Brian Swetland
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.frotz.sonos;
+
+public class Sonos {
+ SoapRPC.Endpoint xport;
+ SoapRPC.Endpoint media;
+ SoapRPC.Endpoint render;
+ SoapRPC rpc;
+
+ public Sonos(byte[] ip) {
+ rpc = new SoapRPC(ip, 1400);
+
+ xport = new SoapRPC.Endpoint(
+ "AVTransport:1",
+ "/MediaRenderer/AVTransport/Control");
+ media = new SoapRPC.Endpoint(
+ "ContentDirectory:1",
+ "/MediaServer/ContentDirectory/Control");
+ render = new SoapRPC.Endpoint(
+ "RenderingControl:1",
+ "/MediaRenderer/RenderingControl/Control");
+ }
+ public void debug(boolean x) {
+ rpc.trace = x;
+ }
+ public void volume() {
+ rpc.call(render,"GetVolume",
+ "<InstanceID>0</InstanceID>"+
+ "<Channel>Master</Channel>" // Master | LF | RF
+ );
+ }
+ public void volume(int vol) { // 0-100
+ if ((vol < 0) || (vol > 100))
+ return;
+ rpc.call(render,"SetVolume",
+ "<InstanceID>0</InstanceID>"+
+ "<Channel>Master</Channel>"+
+ "<DesiredVolume>"+vol+"</DesiredVolume>"
+ );
+ }
+ public void play() {
+ rpc.call(xport,"Play","<InstanceID>0</InstanceID><Speed>1</Speed>");
+ }
+ public void pause() {
+ rpc.call(xport,"Pause","<InstanceID>0</InstanceID>");
+ }
+ public void stop() {
+ rpc.call(xport,"Stop","<InstanceID>0</InstanceID>");
+ }
+ public void next() {
+ rpc.call(xport,"Next","<InstanceID>0</InstanceID>");
+ }
+ public void seekTrack(String nr) {
+ rpc.call(xport,"Seek","<InstanceID>0</InstanceID><Unit>TRACK_NR</Unit><Target>"+nr+"</Target>");
+ // does not start playing if not already in playback mode
+ }
+ public void prev() {
+ rpc.call(xport,"Previous","<InstanceID>0</InstanceID>");
+ }
+ public void remove(String id) {
+ rpc.call(xport,"RemoveTrackFromQueue","<InstanceID>0</InstanceID><ObjectID>"+id+"</ObjectID>");
+ }
+ public void removeAll() {
+ rpc.call(xport,"RemoveAllTracksFromQueue","<InstanceID>0</InstanceID>");
+ }
+ public void add(String uri) {
+ rpc.call(xport,"AddURIToQueue",
+ "<InstanceID>0</InstanceID>"+
+ "<EnqueuedURI>"+uri+"</EnqueuedURI>"+ // from <res> x-file-cifs... etc
+ "<EnqueuedURIMetaData></EnqueuedURIMetaData>"+
+ "<DesiredFirstTrackNumberEnqueued>0</DesiredFirstTrackNumberEnqueued>"+
+ "<EnqueueAsNext>0</EnqueueAsNext>" // 0 = append, 1-n = insert
+ );
+ }
+ public void move(String from, String to) {
+ rpc.call(xport,"ReorderTracksInQueue",
+ "<InstanceID>0</InstanceID>"+
+ "<StartingIndex>"+from+"</StartingIndex>"+
+ "<NumberOfTracks>1</NumberOfTracks>"+
+ "<InsertBefore>"+to+"</InsertBefore>"
+ );
+ }
+ public void list(String _id, boolean d) {
+ int n = 0;
+ XML result = new XML(32768);
+ XML xml = rpc.call(media,"Browse",
+ "<ObjectID>"+_id+"</ObjectID>"+
+ (!d ? "<BrowseFlag>BrowseMetadata</BrowseFlag>" :
+ "<BrowseFlag>BrowseDirectChildren</BrowseFlag>" )+
+ "<Filter></Filter>"+
+ "<StartingIndex>" + n + "</StartingIndex>"+
+ "<RequestedCount>25</RequestedCount>"+
+ "<SortCriteria></SortCriteria>"
+ );
+ try {
+ XML.Sequence name = new XML.Sequence();
+ XML.Sequence value = new XML.Sequence();
+ xml.open("u:BrowseResponse");
+ XML.Sequence tmp = xml.read("Result");
+ xml.unescape(tmp);
+ //System.out.println(tmp);
+ result.init(tmp);
+
+ System.err.println("Count = " + xml.read("NumberReturned"));
+ System.err.println("Total = " + xml.read("TotalMatches"));
+ System.err.println("UpdID = " + xml.read("UpdateID"));
+
+ result.open("DIDL-Lite");
+ while (result.more()) {
+ n++;
+ CharSequence id = result.getAttr("id").copy();
+ CharSequence title = "";
+ CharSequence album = "";
+ CharSequence res = "";
+ String thing = "item";
+ try {
+ result.open("item");
+ } catch (XML.Oops x) {
+ result.open("container"); // yuck!
+ thing = "container";
+ }
+ while (result.tryRead(name,value)) {
+ if ("dc:title".contentEquals(name)) {
+ title = xml.unescape(value).copy();
+ continue;
+ }
+ if ("upnp:album".contentEquals(name)) {
+ album = xml.unescape(value).copy();
+ continue;
+ }
+ if ("res".contentEquals(name)) {
+ res = value.copy();
+ continue;
+ }
+ }
+ if (thing == "item")
+ System.err.println("Item: " + n);
+ else
+ System.err.println("Item: " + id);
+ System.out.println(" Title: " + title);
+ if (album.length() > 0)
+ System.out.println(" Album: " + album);
+ System.out.println(" Resource: " + res);
+ result.close(thing);
+ }
+ } catch (XML.Oops x) {
+ System.err.println("OOPS: " + x.getMessage());
+ x.printStackTrace();
+ }
+ }
+}
diff --git a/net/frotz/sonos/Util.java b/net/frotz/sonos/Util.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2011 Brian Swetland
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.frotz.sonos;
+
+import java.io.File;
+import java.io.FileInputStream;
+
+public class Util {
+ public static byte[] loadFile(String name) {
+ try {
+ File f = new File(name);
+ FileInputStream in = new FileInputStream(f);
+ int sz = (int) f.length();
+ byte[] data = new byte[sz];
+ while (sz > 0) {
+ int r = in.read(data, data.length - sz, sz);
+ if (r <= 0)
+ return null;
+ sz -= r;
+ }
+ return data;
+ } catch (Exception x) {
+ return null;
+ }
+ }
+}
diff --git a/net/frotz/sonos/XML.java b/net/frotz/sonos/XML.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2011 Brian Swetland
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.frotz.sonos;
+
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+
+import java.util.regex.Pattern;
+import java.util.regex.Matcher;
+
+// TODO: ' -> '
+
+public class XML {
+ XML.Sequence seq; /* entire buffer */
+ XML.Sequence tag; /* most recent tag */
+ XML.Sequence tmp; /* for content return */
+ char[] xml;
+ int offset;
+ int count;
+
+ Matcher mTag;
+ Matcher mEntity;
+ Matcher mAttr;
+ boolean isOpen;
+
+ /* used for io operations */
+ CharBuffer buf;
+
+ public XML(int size) {
+ seq = new XML.Sequence();
+ tag = new XML.Sequence();
+ tmp = new XML.Sequence();
+ mTag = pTag.matcher(seq);
+ mEntity = pEntity.matcher(tmp);
+ mAttr = pAttr.matcher(tmp);
+ xml = new char[size];
+ buf = CharBuffer.wrap(xml);
+ }
+
+ public void init(ByteBuffer in) {
+ buf.clear();
+ CoderResult cr = decoder.decode(in, buf, true);
+ System.err.println("cr = " + cr);
+ buf.flip();
+ reset();
+ }
+ public void init(XML.Sequence s) {
+ buf.clear();
+ buf.put(s.data, s.offset, s.count);
+ buf.flip();
+ reset();
+ }
+ void reset() {
+ seq.init(xml, buf.arrayOffset(), buf.length());
+ tag.init(xml, 0, 0);
+ tmp.init(xml, 0, 0);
+ offset = 0;
+ nextTag();
+ System.err.println("XML reset, "+buf.length()+" bytes.");
+ }
+ public XML.Sequence unescape(XML.Sequence s) {
+ int n = s.offset;
+ int max = n + s.count;
+ char data[] = s.data;
+ int out = n;
+
+ while (n < max) {
+ char c = data[n++];
+ if (c != '&') {
+ data[out++] = c;
+ continue;
+ }
+ int e = n;
+ while (n < max) {
+ if (data[n++] != ';')
+ continue;
+ switch(data[e]) {
+ case 'l': // lt
+ data[out++] = '<';
+ break;
+ case 'g': // gt
+ data[out++] = '>';
+ break;
+ case 'q': // quot
+ data[out++] = '"';
+ break;
+ case 'a': // amp | apos
+ if (data[e+1] == 'm')
+ data[out++] = '&';
+ else if (data[e+1] == 'p')
+ data[out++] = '\'';
+ break;
+ }
+ break;
+ }
+ }
+ s.count = out - s.offset;
+ return s;
+ }
+
+ public XML.Sequence getAttr(String name) {
+ int off = mTag.start(3);
+ int end = off + mTag.end(3);
+
+ tmp.offset = 0;
+ tmp.count = end;
+ while (mAttr.find(off)) {
+ //System.err.println("ANAME: " + mAttr.group(1));
+ //System.err.println("ATEXT: " + mAttr.group(2));
+ tmp.offset = mAttr.start(1);
+ tmp.count = mAttr.end(1) - tmp.offset;
+ if (name.contentEquals(tmp)) {
+ tmp.offset = mAttr.start(2);
+ tmp.count = mAttr.end(2) - tmp.offset;
+ return tmp;
+ }
+ tmp.offset = 0;
+ tmp.count = end;
+ off = mAttr.end();
+ }
+ return null;
+ }
+
+ public boolean more() {
+ return isOpen;
+ }
+
+ /* require <tag> and consume it */
+ public void open(String name) throws XML.Oops {
+ if (!isOpen || !name.contentEquals(tag))
+ throw new XML.Oops("expecting <"+name+"> but found " + str());
+ nextTag();
+ }
+
+ /* require </tag> and consume it */
+ public void close(String name) throws XML.Oops {
+ if (isOpen || !name.contentEquals(tag))
+ throw new XML.Oops("expecting </"+name+"> but found " + str());
+ nextTag();
+ }
+
+ /* require <tag> text </tag> and return text */
+ public XML.Sequence read(String name) throws XML.Oops {
+ int start = mTag.end();
+ open(name);
+ tmp.adjust(start, mTag.start());
+ close(name);
+ return tmp;
+ }
+
+ /* read the next <name> value </name> returns false if no open tag */
+ public boolean tryRead(XML.Sequence name, XML.Sequence value) throws XML.Oops {
+ if (!isOpen)
+ return false;
+
+ name.data = xml;
+ name.offset = tag.offset;
+ name.count = tag.count;
+
+ value.data = xml;
+ value.offset = mTag.end();
+
+ nextTag();
+
+ value.count = mTag.start() - value.offset;
+ close(name);
+
+ return true;
+ }
+ public void close(XML.Sequence name) throws XML.Oops {
+ if (isOpen)
+ throw new XML.Oops("1expected </"+name+">, found <"+tag+">");
+ if (!name.eq(tag))
+ throw new XML.Oops("2expected </"+name+">, found </"+tag+">");
+ nextTag();
+ }
+
+ public boolean tryRead(String name, XML.Sequence value) throws XML.Oops {
+ if (!isOpen || !name.contentEquals(tag))
+ return false;
+ value.data = xml;
+ value.offset = mTag.end();
+ nextTag();
+ value.count = mTag.start() - value.offset;
+ close(name);
+ return true;
+ }
+
+ /* eat the current tag and any children */
+ public void consume() throws XML.Oops {
+ tmp.offset = mTag.start(2);
+ tmp.count = mTag.end(2) - tmp.offset;
+ nextTag();
+ while (isOpen)
+ consume();
+ close(tmp);
+ }
+
+ /* format current begin/end tag as a string. for error messages */
+ String str() {
+ if (isOpen)
+ return "<" + tag + ">";
+ else
+ return "</" + tag + ">";
+ }
+
+ void nextTag() {
+ /* can't deal with comments or directives */
+ if (!mTag.find(offset)) {
+ tag.adjust(0,0);
+ return;
+ }
+
+ isOpen = (mTag.start(1) == -1);
+ tag.adjust(mTag.start(2), mTag.end(2));
+ offset = mTag.end();
+ }
+
+ /* G1: \ G2: tagname G3: attributes */
+ static Pattern pTag = Pattern.compile("<(/)?([a-zA-Z:_][a-zA-Z0-9:\\-\\._]*)([^>]*)>",Pattern.DOTALL);
+ /* G1: name G2: value */
+ static Pattern pAttr = Pattern.compile("\\s*([a-zA-Z:_][a-zA-Z0-9:\\-\\._]*)=\"([^\"]*)\"");
+ /* G1: pretext G2: entity G3: entity body */
+ static Pattern pEntity = Pattern.compile("([^&]*)(&([^;]*);)");
+
+ static Charset cs = Charset.forName("UTF-8");
+ static CharsetDecoder decoder = cs.newDecoder();
+
+ static public class Oops extends Exception {
+ public Oops(String msg) {
+ super(msg);
+ }
+ }
+
+ static class Sequence implements CharSequence {
+ private char[] data;
+ private int offset;
+ private int count;
+
+ public Sequence() {
+ }
+
+ void init(char[] data, int start, int end) {
+ this.data = data;
+ offset = start;
+ count = end - start;
+ }
+ void adjust(int start, int end) {
+ offset = start;
+ count = end - start;
+ }
+ boolean eq(Sequence other) {
+ int count = this.count;
+ if (count != other.count)
+ return false;
+ char[] a = this.data;
+ int ao = this.offset;
+ char[] b = other.data;
+ int bo = other.offset;
+ while (count-- > 0)
+ if (a[ao++] != b[bo++])
+ return false;
+ return true;
+ }
+ CharSequence copy() {
+ Sequence s = new Sequence();
+ s.init(data, offset, offset + count);
+ return s;
+ }
+
+ /* CharSequence interface */
+ public int length() {
+ return count;
+ }
+ public char charAt(int index) {
+ //System.err.print("["+data[offset+index]+"]");
+ return data[offset + index];
+ }
+ public CharSequence subSequence(int start, int end) {
+ //System.err.println("[subSequence("+start+","+end+")]");
+ Sequence x = new Sequence();
+ x.init(data, offset + start, offset + end);
+ return x;
+ }
+ public String toString() {
+ return new String(data, offset, count);
+ }
+ }
+}
diff --git a/net/frotz/sonos/app.java b/net/frotz/sonos/app.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2011 Brian Swetland
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.frotz.sonos;
+
+public class app {
+ public static void main(String args[]) {
+ Sonos sonos = new Sonos(new byte[] { 10, 0, 0, (byte) 199});
+ String cmd = args[0];
+
+// sonos.debug(true);
+
+ if (cmd.equals("play")) {
+ sonos.play();
+ } else if (cmd.equals("pause")) {
+ sonos.pause();
+ } else if (cmd.equals("next")) {
+ sonos.next();
+ } else if (cmd.equals("prev")) {
+ sonos.prev();
+ } else if (cmd.equals("list")) {
+ sonos.list(args[1],true);
+ } else if (cmd.equals("dump")) {
+ sonos.list(args[1],false);
+ } else if (cmd.equals("add")) {
+ sonos.add(args[1]);
+ } else if (cmd.equals("remove")) {
+ sonos.remove(args[1]);
+ } else if (cmd.equals("removeall")) {
+ sonos.removeAll();
+ } else if (cmd.equals("move")) {
+ sonos.move(args[1],args[2]);
+ } else if (cmd.equals("track")) {
+ sonos.seekTrack(args[1]);
+ } else if (cmd.equals("volume")) {
+ if (args.length == 1)
+ sonos.volume();
+ else
+ sonos.volume(Integer.parseInt(args[1]));
+ } else {
+ System.err.println("Unknown command '"+cmd+"'");
+ System.exit(-1);
+ }
+ }
+}