sonos

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs

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:
A.gitignore | 3+++
AMETA-INF/MANIFEST.MF | 3+++
AMakefile | 12++++++++++++
Anet/frotz/sonos/SoapRPC.java | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anet/frotz/sonos/Sonos.java | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anet/frotz/sonos/Util.java | 40++++++++++++++++++++++++++++++++++++++++
Anet/frotz/sonos/XML.java | 307+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Anet/frotz/sonos/app.java | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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: &apos; -> ' + +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); + } + } +}