From 64635446e378ad94a1cede4117e73aa010e29a4c Mon Sep 17 00:00:00 2001 From: "stephen@sfnelson.org" Date: Mon, 2 Feb 2009 00:38:23 +0000 Subject: [PATCH] New client git-svn-id: https://stereo.googlecode.com/svn/trunk@394 c67ee986-0855-0410-825f-15918b819f62 --- Asunder/webserver/spotlight/daap.js | 375 +++++++++++++++++++++++++++++++++ Asunder/webserver/spotlight/dacp.js | 233 ++++++++++++++++++++ Asunder/webserver/spotlight/index.html | 38 ++++ Asunder/webserver/spotlight/style.css | 149 +++++++++++++ Asunder/webserver/spotlight/views.js | 201 ++++++++++++++++++ 5 files changed, 996 insertions(+) create mode 100644 Asunder/webserver/spotlight/daap.js create mode 100644 Asunder/webserver/spotlight/dacp.js create mode 100644 Asunder/webserver/spotlight/index.html create mode 100644 Asunder/webserver/spotlight/style.css create mode 100644 Asunder/webserver/spotlight/views.js diff --git a/Asunder/webserver/spotlight/daap.js b/Asunder/webserver/spotlight/daap.js new file mode 100644 index 0000000..ff75f5b --- /dev/null +++ b/Asunder/webserver/spotlight/daap.js @@ -0,0 +1,375 @@ +var constants = { + put: function (name, code, type) { + var n = name.split('.'); + var c = this; + for (i in n) { + if (i+1 < n.length) { + c = c[n[i]]; + } + else { + c[n[i]] = new Constant(name, code, type); + } + } + }, + dmap: { + contentcodesresponse: new Constant("dmap.contentcodesresponse", "mccr", 12), + dictionary: new Constant("dmap.dictionary", "mdcl", 12), + contentcodesnumber: new Constant("dmap.contentcodesnumber", "mcnm", 5), + contentcodesname: new Constant("dmap.contentcodesname", "mcna", 9), + contentcodestype: new Constant("dmap.contentcodestype", "mcty", 3) + }, + daap: new Object(), + dacp: new Object(), + dmcp: new Object(), + com: { apple: { itunes: new Object() } } +}; + +function Constant(name, code, type) { + this.name = name; + this.code = code; + this.type = type; +} + +function DACPNode(text) { + + var parseNum = function (index, len) { + if (len > 4) return parseBigNum(index, len); + + var num = text.charCodeAt(index) & 255; + for (var i = 1; i < len; i++) { + num = num * 256; + num += text.charCodeAt(index + i) & 255; + } + return num; + }; + + var parseBigNum = function (index, len) { + var num = new BigNumber(text.charCodeAt(index) & 255); + for (var i = 1; i < len; i++) { + num = num.multiply(256); + num = num.add(text.charCodeAt(index + i) & 255); + } + return num; + }; + + this.name = function (index) { + return text.substring(index, index+4); + }; + this.length = function (index) { + return parseNum(index+4, 4); + }; + this.byteVal = function (index) { + return parseNum(index+8, 1); + }; + this.shortVal = function (index) { + return parseNum(index+8, 2); + }; + this.intVal = function (index) { + return parseNum(index+8, 4); + }; + this.longVal = function (index) { + return parseNum(index+8, 8); + }; + this.longlongVal = function (index) { + return { + 0: parseNum(index+8, 4), + 1: parseNum(index+12, 4), + 2: parseNum(index+16, 4), + 3: parseNum(index+20, 4) + }; + }; + this.stringVal = function (index) { + var length = this.length(index); + var string = ""; + for ( var i = 8; i < 8+length;) { + var c = text.charCodeAt(index + i) & 255; + var c2,c3; + if (c < 128) { + string += String.fromCharCode(c); + i++; + } else if (c > 191 && c < 224) { + c2 = text.charCodeAt(index + i + 1) & 255; + string += String.fromCharCode(((c & 31)) << 6 | (c2 & 63)); + i += 2; + } else { + c2 = text.charCodeAt(index + i + 1) & 255; + c3 = text.charCodeAt(index + i + 2) & 255; + string += String.fromCharCode(((c & 15)) << 12 | (c2 & 63) << 6 + | (c3 & 63)); + i += 3; + } + } + return string; + }; + this.dateVal = function (index) { + return parseNum(index+8, 8); + }; + this.versionVal = function (index) { + var version = text.charCodeAt(index+8) & 255; + for (i in 1, 2, 3) { + version += "."; + version += text.charCodeAt(index+8 + i) & 255; + } + return version; + }; + this.children = function (index) { + var kids = new Array(); + var len = index + 8 + this.length(index); + var i = index+8; + while (i < len) { + kids.push(i); + i += 8 + this.length(i); + } + return kids; + }; + this.findNode = function (index, tag) { + var kids = this.children(index); + for (child in kids) { + if (this.name(kids[child]) == tag) { + return kids[child]; + } + } + return -1; + } +} + +function DACPPlayStatusUpdate(tree, index) { + + var kids = tree.children(index); + + this.album = ""; + this.title = ""; + this.artist = ""; + this.genre = ""; + this.id = 0; + this.status = 0; + this.revision = 0; + this.container = 0; + + for (child in kids) { + var node = kids[child]; + switch (tree.name(node)) { + case "caps": + this.status = tree.byteVal(node); + break; + case "cmsr": + this.revision = tree.intVal(node); + break; + case "cann": + this.title = tree.stringVal(node); + break; + case "cana": + this.artist = tree.stringVal(node); + break; + case "canl": + this.album = tree.stringVal(node); + break; + case "cang": + this.genre = tree.stringVal(node); + break; + case "canp": + this.container = tree.longlongVal(node)[1]; + this.id = tree.longlongVal(node)[3]; + break; + } + } +} + +function getItems(tree, index, handler) { + + var items = new Array(); + + var kids = tree.children(index); + for (child in kids) { + items.push(handler(tree, kids[child])); + } + + return items; +} + +function DACPContainer(tree, index) { + + this.id = 0; + this.persistent = 0; + this.name = ""; + this.base = false; + this.parent = 0; + this.items = 0; + + var kids = tree.children(index); + for (child in kids) { + var node = kids[child]; + switch (tree.name(node)) { + case constants.dmap.itemid.code: + this.id = tree.intVal(node); + break; + case constants.dmap.persistentid.code: + this.persistent = tree.longVal(node); + break; + case constants.dmap.itemname.code: + this.name = tree.stringVal(node); + break; + case constants.daap.baseplaylist.code: + this.base = tree.byteVal(node); + break; + case constants.dmap.parentcontainerid.code: + this.parent = tree.intVal(node); + break; + case constants.dmap.itemcount.code: + this.items = tree.intVal(node); + break; + } + } + + this.init(); + this.node.className += " -database-playlist"; + + var pl = new View(); + pl.init(this.name); + pl.collapse(); + this.appendChild(pl.wrapper); + //this.appendChild(this.name, "-database-playlist-name"); + //this.appendChild(this.items, "-database-playlist-items"); + //this.appendChild(this.id, "-database-playlist-id"); +} +DACPContainer.prototype = new Item; + +function DACPPlaylist(tree, index) { + + this.tracks = new Array(); + + var kids = tree.children(index); + var list = 0; + for (child in kids) { + if (tree.name(kids[child]) == "mlcl") { + list = kids[child]; + break; + } + } + + if (!list) return; + + this.init(); + + kids = tree.children(list); + for (child in kids) { + this.tracks.push(new DACPTrack(tree, kids[child])); + } + +} +DACPPlaylist.prototype = new View; + +function DACPAlbum(tree, index) { + + this.id = 0; + this.name = ""; + this.artist = 0; + this.persistantId = 0; + + var kids = tree.children(index); + for (child in kids) { + var node = kids[child]; + switch (tree.name(node)) { + case "miid": + this.id = tree.intVal(node); + break; + case "minm": + this.name = tree.stringVal(node); + break; + case "asaa": + this.artist = tree.stringVal(node); + break; + case "mper": + this.persistantId = tree.longVal(node); + break; + } + } + + this.init(); + this.node.className += " -stereo-album"; + + this.appendChild(this.name, "-stereo-album-title"); + this.appendChild(this.title, "-stereo-album-artist"); +} +DACPAlbum.prototype = new Item; + +function DACPTrack(tree, index) { + + this.album = ""; + this.title = ""; + this.artist = ""; + this.genre = ""; + this.id = 0; + this.persistent = 0; + + var kids = tree.children(index); + for (child in kids) { + var node = kids[child]; + switch (tree.name(node)) { + case "asal": + this.album = tree.stringVal(node); + break; + case "minm": + this.title = tree.stringVal(node); + break; + case "asar": + this.artist = tree.stringVal(node); + break; + case "asag": + this.genre = tree.stringVal(node); + break; + case "miid": + this.id = tree.intVal(node); + break; + case "mper": + this.persistent = tree.longVal(node); + break; + } + } + + this.init(); + this.node.className += " -stereo-track"; + + this.appendChild(this.artist, "-stereo-track-artist"); + this.appendChild(this.title, "-stereo-track-title"); + this.appendChild(this.album, "-stereo-track-album"); + this.appendChild(this.genre, "-stereo-track-genre"); +} +DACPTrack.prototype = new Item; + +function BrowseItem(tree, index, type) { + + this.type = type; + this.value = tree.stringVal(index); + + this.init(); + this.node.className += " -browse-item"; + this.appendChild("→", "hover enqueue"); + this.appendChild(this.value); + + this.clicked = function (e) { + if (e.target.className && e.target.className.indexOf("enqueue") != -1) { + dacp.enqueue(this.type.name + ":" + this.value); + } + else { + BrowseItem.prototype.clicked.call(this); + } + }; + +} +BrowseItem.prototype = new Item; +BrowseItem.prototype.selected = function () { + dacp.setSearch(this.type.name + ":" + this.value); +}; + +function sysout(text) { + if (window.console) { + window.console.log(text); + } + else { + var p = document.createElement("PRE"); + p.className = "debug"; + p.appendChild(document.createTextNode(text)); + document.body.appendChild(p); + } +} diff --git a/Asunder/webserver/spotlight/dacp.js b/Asunder/webserver/spotlight/dacp.js new file mode 100644 index 0000000..de3ad5e --- /dev/null +++ b/Asunder/webserver/spotlight/dacp.js @@ -0,0 +1,233 @@ +function DACP (host, container) { + + var dis = this; + + this.host = host; + this.container = container; + + this.current = 0; + var requests = new Queue(); + + this.request = function (request) { + if (this.current) { + requests.offer(request); + } + else { + this.current = request; + ajax.request(this.host + request, function (text) { dis.response(text); }); + } + }; + + this.response = function (response) { + + if (response.length == 0) return; + + var tree = new DACPNode(response); + var node = 0; + switch (tree.name(0)) { + case constants.dmap.contentcodesresponse.code: + node = this.contentCodes(tree); + break; + case constants.daap.databaseplaylists.code: + node = new DatabasePlaylists(tree); + break; + case constants.daap.databasebrowse.code: + node = new BrowseResponse(tree); + break; + case constants.daap.playlistsongs.code: + node = new PlaylistSongs(tree); + break; + default: + alert(tree.name(0) + " not found"); + } + + if (node && node.items.length > 0) { + this.container.appendChild(node.wrapper); + } + + this.current = 0; + if (requests.size() > 0) { + this.request(requests.poll()); + } + }; + + this.contentCodes = function (tree) { + + var parse = function (tree, entry) { + var name; + var code; + var type; + var nodes = tree.children(entry); + for (n in nodes) { + var node = nodes[n]; + switch (tree.name(node)) { + case constants.dmap.contentcodesname.code: + name = tree.stringVal(node); + break; + case constants.dmap.contentcodestype.code: + type = tree.intVal(node); + break; + case constants.dmap.contentcodesnumber.code: + code = tree.stringVal(node); + break; + } + } + constants.put(name, code, type); + }; + + var children = tree.children(0); + for (i in children) { + var child = children[i]; + if (tree.name(child) == constants.dmap.dictionary.code) { + parse(tree, child); + } + } + }; + + this.search = function () { + + var query = document.getElementById("search-field").value; + + while (this.container.firstChild) { + this.container.removeChild(this.container.firstChild); + } + + if (query.indexOf(":") != -1) { + query = "daap.song"+query; + dacp.request("/databases/1/browse/artists?filter='"+query+"'"); + dacp.request("/databases/1/browse/albums?filter='"+query+"'"); + dacp.request("/databases/1/containers/1/items?query='"+query+"'"); + } + else { + dacp.request("/databases/1/browse/artists?filter='daap.songartist:*"+query+"*'"); + dacp.request("/databases/1/browse/albums?filter='daap.songalbum:*"+query+"*'"); + dacp.request("/databases/1/containers/1/items?query='dmap.itemname:*"+query+"*'"); + } + }; + + this.setSearch = function (query) { + if (query.indexOf(":") != -1) { + query = query.replace("daap.song",""); + } + var field = document.getElementById("search-field"); + field.value = query; + this.search(); + }; + + this.setSearchCategory = function (type) { + var field = document.getElementById("search-field"); + query = field.value; + + if (query.indexOf(":") != -1) { + query = query.substring(query.indexOf(":")+1); + } + else if (query.charAt(0) != "*") { + query = "*"+query+"*"; + } + + query = type.name + ":" + query; + query = query.replace("daap.song",""); + + this.setSearch(query); + }; + + this.enqueue = function (query) { + this.request("/ctrl-int/1/cue?query='"+query+"'"); + } + + this.request("/content-codes"); +} + +function DatabasePlaylists(tree) { + this.init(); + + var list = tree.findNode(0, constants.dmap.listing.code); + if (list == -1) { + alert("invalid database containers response"); + return; + } + + var children = tree.children(list); + + for (var i in children) { + this.put(new DACPContainer(tree, children[i])); + } + + this.wrapper.className += " -stereo-database-containers" + this.show(); +} +DatabasePlaylists.prototype = new View; + +function BrowseResponse(tree) { + + var list = -1; + + var children = tree.children(0); + for (var i in children) { + var child = children[i]; + switch (tree.name(child)) { + case constants.daap.browseartistlisting.code: + this.name = "Artists"; list = child; + this.type = constants.daap.songartist; + break; + case constants.daap.browsealbumlisting.code: + this.name = "Albums"; list = child; + this.type = constants.daap.songalbum; + break; + case constants.daap.browsegenrelisting.code: + this.name = "Genres"; list = child; + this.type = constants.daap.songgenre; + break; + case constants.daap.browsecomposerlisting.code: + this.name = "Composers"; list = child; + this.type = constants.daap.songcomposer; + break; + default: + continue; + } + } + + if (list == -1) { + alert("invalid database browse response"); + this.init(); + return; + } + + this.init(this.name); + + var children = tree.children(list); + + for (var i in children) { + this.put(new BrowseItem(tree, children[i], this.type)); + } + + this.wrapper.className += " database-browse" + this.show(); + + var type = this.type; + this.activate = function () { + dacp.setSearchCategory(type); + }; +} +BrowseResponse.prototype = new View; + +function PlaylistSongs(tree) { + this.init("Songs"); + + var list = tree.findNode(0, constants.dmap.listing.code); + if (list == -1) { + alert("invalid playlist songs response"); + return; + } + + var children = tree.children(list); + + for (var i in children) { + this.put(new DACPTrack(tree, children[i])); + } + + this.wrapper.className += " playlist-songs" + this.show(); + +} +PlaylistSongs.prototype = new View; diff --git a/Asunder/webserver/spotlight/index.html b/Asunder/webserver/spotlight/index.html new file mode 100644 index 0000000..40d360c --- /dev/null +++ b/Asunder/webserver/spotlight/index.html @@ -0,0 +1,38 @@ + + + + + Stereo + + + + + + + + + +
+ +
+ + + + diff --git a/Asunder/webserver/spotlight/style.css b/Asunder/webserver/spotlight/style.css new file mode 100644 index 0000000..0defeb3 --- /dev/null +++ b/Asunder/webserver/spotlight/style.css @@ -0,0 +1,149 @@ +html,body { + margin: 0; + margin-left: -1px; + padding: 0; + font-family: "Lucida Grande", sans-serif; +} + +.-stereo-container { + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.-stereo-container-inner { + padding-bottom: 2ex; +} + +.collapsed .-stereo-container-inner { + display: none; +} + +.-stereo-track div { + display: none; +} + +.-stereo-track.selected div { + display: block; +} + +.-stereo-track .-stereo-track-title { + display: block; +} + +#playing { + padding-left: 1em; + width: 20em; + height: 100%; + overflow: auto; + position: fixed; + border-right: solid 0.5em #0E37E7; +} + +#browse { + margin-left: 21em; +} + +#browse .-stereo-container-title { + float: left; + padding: 1ex 0 0 1em; +} + +#browse .-stereo-container { + clear: both; +} + +#browse .-stereo-container-inner { + margin-left: -8em; + padding: 0.5ex 0; + margin-left: 8em; + border-left: solid 1px #aaa; +} + +#browse .-stereo-container-item { + margin-left: -8em; + padding-left: 8em; +} + +#browse .-stereo-container-item div { + margin-left: -1px; + padding: 0.5ex 0; + padding-left: 1em; + border-left: solid thin #aaa; +} + +#browse .-stereo-container-item:hover { + background-color: #0E37E7; + color: white; +} + +#browse .-stereo-container-item:hover div { + border-color: white; +} + +#browse .-stereo-container-item .hover { + display: none; +} + +#browse .-stereo-container-item:hover .hover { + display: block; + float: right; + border: none; + font-weight: bold; + text-align: center; + -webkit-border-radius: 1.1ex; + -moz-border-radius: 1.1ex; + width: 2em; + height: 1.2em; + line-height: 1em; + padding: 0; + margin: 0.5ex 1em 0.5ex 0; + background-color: white; + color: #0E37E7; +} + +#search { + top: 0; + right: 0; + margin-left: 21em; + padding-top: 0.5ex; + width: auto; + background-color: #0E37E7; + overflow: auto; + text-align: right; + height: 4ex; +} + +#search label { + color: white; +} + +#search input { + font-size: 1em; +} + +#search .search-field { + margin-right: 0.5em; + padding: 0.5ex 1.5ex; + border: none; + outline: none; + -webkit-border-radius: 1.5ex; + -moz-border-radius: 1.5ex; + border-radius: 1.5ex; +} + +#search .search-reset { + position: absolute; + background-color: #bababa; + border: none; + font-weight: bold; + -webkit-border-radius: 1ex; + -moz-border-radius: 1ex; + width: 1.2em; + height: 1.2em; + line-height: 1em; + padding: 0; + color: white; + margin-left: -2.25em; + margin-top: 0.75ex; +} \ No newline at end of file diff --git a/Asunder/webserver/spotlight/views.js b/Asunder/webserver/spotlight/views.js new file mode 100644 index 0000000..a378b00 --- /dev/null +++ b/Asunder/webserver/spotlight/views.js @@ -0,0 +1,201 @@ +function View(title) { + + this.init = function (titleText) { + + if (this.prototype && this.prototype.init) this.prototype.init(); + + var view = this; + + this.wrapper = document.createElement("DIV"); + this.wrapper.className = "-stereo-container"; + this.wrapper.addEventListener("mousemove", function(e) { + if (view.dragging) { + e.stopPropagation(); + var t = e.target; + while (t != document.body) { + if (t.parentNode == view.container) { + if (view.dragging.node.nextSibling && view.dragging.node.nextSibling == t) { + view.container.insertBefore(t, view.dragging.node); + } + else if (t != view.dragging.node) { + view.container.insertBefore(view.dragging.node, t); + } + break; + } + t = t.parentNode; + } + } + }, false); + this.wrapper.addEventListener("mouseup", function(e) { + if (view.dragging) { + e.stopPropagation(); + view.dragging = 0; + } + }, false); + + this.container = document.createElement("DIV"); + this.container.className = "-stereo-container-inner"; + this.items = new Array(); + + if (titleText) { + this.title = document.createElement("DIV"); + this.title.className = "-stereo-container-title"; + this.title.appendChild(document.createTextNode(titleText)); + + this.title.addEventListener("click", function (e) { + e.stopPropagation(); + view.activate(); + }, false); + + this.wrapper.appendChild(this.title); + } + + this.wrapper.appendChild(this.container); + + this.node = function () { + return this.wrapper; + }; + }; + + this.collapse = function () { + this.wrapper.className += " collapsed"; + this.collapsed = true; + }; + + this.show = function () { + this.refresh(); + this.wrapper.className = this.wrapper.className.replace("collapse",""); + this.collapsed = false; + }; + + this.refresh = function () { + while (this.container.firstChild) { + this.container.removeChild(this.container.firstChild); + } + + for (i in this.items) { + this.container.appendChild(this.items[i].node); + } + }; + + this.put = function (id, item) { + if (!item) { + this.items.push(id); + id.container = this; + } + else { + this.items[id] = item; + item.container = this; + } + }; + + this.get = function (id) { + return this.items[id]; + }; + + this.sort = function () { + this.items.sort(); + }; + + this.clear = function () { + this.items = new Array(); + }; + + this.select = function (item) { + if (this.selected) { + this.selected.deselected(); + } + + if (this.selected == item) { + this.selected = 0; + } + else { + this.selected = item; + item.selected(); + } + }; + + this.activate = function () { + //do nothing by default + }; +} + +function Item() { + + this.init = function () { + this.node = document.createElement("DIV"); + this.node.className = "-stereo-container-item"; + this.container = 0; + + var dis = this; + + this.node.addEventListener("click", function (e) { + e.stopPropagation(); + dis.clicked(e); + }, false); + this.node.addEventListener("mousedown", function (e) { + e.stopPropagation(); + if (dis.container) { + dis.container.dragging = dis; + } + }, false); + }; + this.appendChild = function (inner, cls) { + var node = document.createElement("DIV"); + node.className = cls; + if ('string' == typeof(inner)) { + node.appendChild(document.createTextNode(inner)); + } + else { + node.appendChild(inner); + } + this.node.appendChild(node); + }; + this.selected = function () { + this.node.className += " selected"; + }; + this.deselected = function () { + this.node.className = this.node.className.replace("selected", ""); + }; + this.clicked = function (e) { + if (this.container) { + sysout(this); + sysout(this.container); + this.container.select(this); + } + }; +} + +function Queue() { + var size = 0; + var first = 0; + var last = 0; + + this.size = function () { + return size; + }; + + this.poll = function () { + if (first) { + size--; + var value = first.value; + first = first.next; + if (!first) last = 0; + return value; + } + return 0; + }; + + this.offer = function (value) { + size++; + var n = { next: 0, value: value }; + if (last) { + last.next = n; + last = n; + } + else { + first = n; + last = n; + } + }; +} \ No newline at end of file -- 2.11.4.GIT