mpd.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. /* ympd
  2. (c) 2013-2014 Andrew Karpow <andy@ndyk.de>
  3. This project's homepage is: http://www.ympd.org
  4. This program is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation; version 2 of the License.
  7. This program is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License along
  12. with this program; if not, write to the Free Software Foundation, Inc.,
  13. Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  14. */
  15. var socket;
  16. var last_state;
  17. var current_app;
  18. var lastSongTitle = "";
  19. var current_song = new Object();
  20. var app = $.sammy(function() {
  21. this.before('/', function(e, data) {
  22. $('#nav_links > li').removeClass('active');
  23. });
  24. this.get('#/', function() {
  25. current_app = 'queue';
  26. $('#breadcrump').addClass('hide');
  27. $('#salamisandwich').find("tr:gt(0)").remove();
  28. socket.send('MPD_API_GET_QUEUE');
  29. $('#panel-heading').text("Queue");
  30. $('#queue').addClass('active');
  31. });
  32. this.get(/\#\/browse\/(.*)/, function() {
  33. current_app = 'browse';
  34. $('#breadcrump').removeClass('hide').empty().append("<li><a href=\"#/browse/\">root</a></li>");
  35. $('#salamisandwich').find("tr:gt(0)").remove();
  36. var path = this.params['splat'][0];
  37. socket.send('MPD_API_GET_BROWSE,' + path);
  38. $('#panel-heading').text("Browse database: "+path);
  39. var path_array = path.split('/');
  40. var full_path = "";
  41. $.each(path_array, function(index, chunk) {
  42. if(path_array.length - 1 == index) {
  43. $('#breadcrump').append("<li class=\"active\">"+ chunk + "</li>");
  44. return;
  45. }
  46. full_path = full_path + chunk;
  47. $('#breadcrump').append("<li><a href=\"#/browse/" + full_path + "\">"+chunk+"</a></li>");
  48. full_path += "/";
  49. });
  50. $('#browse').addClass('active');
  51. });
  52. this.get(/\#\/search\/(.*)/, function() {
  53. current_app = 'search';
  54. $('#salamisandwich').find("tr:gt(0)").remove();
  55. var searchstr = this.params['splat'][0];
  56. $('#search > div > input').val(searchstr);
  57. socket.send('MPD_API_SEARCH,' + searchstr);
  58. $('#panel-heading').text("Search: "+searchstr);
  59. });
  60. this.get("/", function(context) {
  61. context.redirect("#/");
  62. });
  63. });
  64. $(document).ready(function(){
  65. webSocketConnect();
  66. $("#volumeslider").slider(0);
  67. $("#volumeslider").on('slider.newValue', function(evt,data){
  68. socket.send("MPD_API_SET_VOLUME,"+data.val);
  69. });
  70. $('#progressbar').slider(0);
  71. $("#progressbar").on('slider.newValue', function(evt,data){
  72. if(current_song && current_song.currentSongId >= 0) {
  73. var seekVal = Math.ceil(current_song.totalTime*(data.val/100));
  74. socket.send("MPD_API_SET_SEEK,"+current_song.currentSongId+","+seekVal);
  75. }
  76. });
  77. if(!notificationsSupported())
  78. $('#btnnotify').addClass("disabled");
  79. else
  80. if ($.cookie("notification") === "true")
  81. $('#btnnotify').addClass("active")
  82. });
  83. function webSocketConnect() {
  84. if (typeof MozWebSocket != "undefined") {
  85. socket = new MozWebSocket(get_appropriate_ws_url());
  86. } else {
  87. socket = new WebSocket(get_appropriate_ws_url());
  88. }
  89. try {
  90. socket.onopen = function() {
  91. console.log("connected");
  92. $('.top-right').notify({
  93. message:{text:"Connected to ympd"},
  94. fadeOut: { enabled: true, delay: 500 }
  95. }).show();
  96. app.run();
  97. }
  98. socket.onmessage =function got_packet(msg) {
  99. if(msg.data === last_state)
  100. return;
  101. var obj = JSON.parse(msg.data);
  102. switch (obj.type) {
  103. case "queue":
  104. if(current_app !== 'queue')
  105. break;
  106. $('#salamisandwich > tbody').empty();
  107. for (var song in obj.data) {
  108. var minutes = Math.floor(obj.data[song].duration / 60);
  109. var seconds = obj.data[song].duration - minutes * 60;
  110. $('#salamisandwich > tbody').append(
  111. "<tr trackid=\"" + obj.data[song].id + "\"><td>" + (obj.data[song].pos + 1) + "</td>" +
  112. "<td>"+ obj.data[song].title +"</td>" +
  113. "<td>"+ minutes + ":" + (seconds < 10 ? '0' : '') + seconds +
  114. "</td><td></td></tr>");
  115. }
  116. $('#salamisandwich > tbody > tr').on({
  117. mouseover: function(){
  118. if($(this).children().last().has("a").length == 0)
  119. $(this).children().last().append(
  120. "<a class=\"pull-right btn-group-hover\" href=\"#/\" " +
  121. "onclick=\"socket.send('MPD_API_RM_TRACK," + $(this).attr("trackid") +"'); $(this).parents('tr').remove();\">" +
  122. "<span class=\"glyphicon glyphicon-trash\"></span></a>")
  123. .find('a').fadeTo('fast',1);
  124. },
  125. click: function() {
  126. $('#salamisandwich > tbody > tr').removeClass('active');
  127. socket.send('MPD_API_PLAY_TRACK,'+$(this).attr('trackid'));
  128. $(this).addClass('active');
  129. },
  130. mouseleave: function(){
  131. $(this).children().last().find("a").stop().remove();
  132. }
  133. });
  134. break;
  135. case "search":
  136. case "browse":
  137. if(current_app !== 'browse' && current_app !== 'search')
  138. break;
  139. for (var item in obj.data) {
  140. switch(obj.data[item].type) {
  141. case "directory":
  142. $('#salamisandwich > tbody').append(
  143. "<tr uri=\"" + obj.data[item].dir + "\" class=\"dir\">" +
  144. "<td><span class=\"glyphicon glyphicon-folder-open\"></span></td>" +
  145. "<td><a>" + basename(obj.data[item].dir) + "</a></td>" +
  146. "<td></td><td></td></tr>"
  147. );
  148. break;
  149. case "playlist":
  150. $('#salamisandwich > tbody').append(
  151. "<tr uri=\"" + obj.data[item].plist + "\" class=\"plist\">" +
  152. "<td><span class=\"glyphicon glyphicon-list\"></span></td>" +
  153. "<td><a>" + basename(obj.data[item].plist) + "</a></td>" +
  154. "<td></td><td></td></tr>"
  155. );
  156. break;
  157. case "song":
  158. var minutes = Math.floor(obj.data[item].duration / 60);
  159. var seconds = obj.data[item].duration - minutes * 60;
  160. $('#salamisandwich > tbody').append(
  161. "<tr uri=\"" + obj.data[item].uri + "\" class=\"song\">" +
  162. "<td><span class=\"glyphicon glyphicon-music\"></span></td>" +
  163. "<td>" + obj.data[item].title +"</td>" +
  164. "<td>"+ minutes + ":" + (seconds < 10 ? '0' : '') + seconds +
  165. "</td><td></td></tr>"
  166. );
  167. break;
  168. }
  169. }
  170. function appendClickableIcon(appendTo, onClickAction, glyphicon) {
  171. $(appendTo).children().last().append(
  172. "<a role=\"button\" class=\"pull-right btn-group-hover\">" +
  173. "<span class=\"glyphicon glyphicon-" + glyphicon + "\"></span></a>")
  174. .find('a').click(function(e) {
  175. e.stopPropagation();
  176. socket.send(onClickAction + "," + $(this).parents("tr").attr("uri"));
  177. $('.top-right').notify({
  178. message:{
  179. text: $('td:nth-child(2)', $(this).parents("tr")).text() + " added"
  180. } }).show();
  181. }).fadeTo('fast',1);
  182. }
  183. $('#salamisandwich > tbody > tr').on({
  184. mouseenter: function() {
  185. if($(this).is(".dir"))
  186. appendClickableIcon($(this), 'MPD_API_ADD_TRACK', 'plus');
  187. else if($(this).is(".song"))
  188. appendClickableIcon($(this), 'MPD_API_ADD_PLAY_TRACK', 'play');
  189. },
  190. click: function() {
  191. if($(this).is(".dir"))
  192. app.setLocation("#/browse/"+$(this).attr("uri"));
  193. else {
  194. if($(this).is(".song"))
  195. socket.send("MPD_API_ADD_TRACK," + $(this).attr("uri"));
  196. else
  197. socket.send("MPD_API_ADD_PLAYLIST," + $(this).attr("uri"));
  198. $('.top-right').notify({
  199. message:{
  200. text: $('td:nth-child(2)', this).text() + " added"
  201. }
  202. }).show();
  203. }
  204. },
  205. mouseleave: function(){
  206. $(this).children().last().find("a").stop().remove();
  207. }
  208. });
  209. break;
  210. case "state":
  211. updatePlayIcon(obj.data.state);
  212. updateVolumeIcon(obj.data.volume);
  213. if(JSON.stringify(obj) === JSON.stringify(last_state))
  214. break;
  215. current_song.totalTime = obj.data.totalTime;
  216. current_song.currentSongId = obj.data.currentsongid;
  217. var total_minutes = Math.floor(obj.data.totalTime / 60);
  218. var total_seconds = obj.data.totalTime - total_minutes * 60;
  219. var elapsed_minutes = Math.floor(obj.data.elapsedTime / 60);
  220. var elapsed_seconds = obj.data.elapsedTime - elapsed_minutes * 60;
  221. $('#volumeslider').slider(obj.data.volume);
  222. var progress = Math.floor(100*obj.data.elapsedTime/obj.data.totalTime);
  223. $('#progressbar').slider(progress);
  224. $('#counter')
  225. .text(elapsed_minutes + ":" +
  226. (elapsed_seconds < 10 ? '0' : '') + elapsed_seconds + " / " +
  227. total_minutes + ":" + (total_seconds < 10 ? '0' : '') + total_seconds);
  228. $('#salamisandwich > tbody > tr').removeClass('active').css("font-weight", "");
  229. $('#salamisandwich > tbody > tr[trackid='+obj.data.currentsongid+']').addClass('active').css("font-weight", "bold");
  230. if(obj.data.random)
  231. $('#btnrandom').addClass("active")
  232. else
  233. $('#btnrandom').removeClass("active");
  234. if(obj.data.consume)
  235. $('#btnconsume').addClass("active")
  236. else
  237. $('#btnconsume').removeClass("active");
  238. if(obj.data.single)
  239. $('#btnsingle').addClass("active")
  240. else
  241. $('#btnsingle').removeClass("active");
  242. if(obj.data.repeat)
  243. $('#btnrepeat').addClass("active")
  244. else
  245. $('#btnrepeat').removeClass("active");
  246. last_state = obj;
  247. break;
  248. case "disconnected":
  249. if($('.top-right').has('div').length == 0)
  250. $('.top-right').notify({
  251. message:{text:"ympd lost connection to MPD "},
  252. type: "danger",
  253. fadeOut: { enabled: true, delay: 1000 },
  254. }).show();
  255. break;
  256. case "update_queue":
  257. if(current_app === 'queue')
  258. socket.send('MPD_API_GET_QUEUE');
  259. break;
  260. case "song_change":
  261. $('#currenttrack').text(" " + obj.data.title);
  262. var notification = "<strong><h4>" + obj.data.title + "</h4></strong>";
  263. if(obj.data.album) {
  264. $('#album').text(obj.data.album);
  265. notification += obj.data.album + "<br />";
  266. }
  267. if(obj.data.artist) {
  268. $('#artist').text(obj.data.artist);
  269. notification += obj.data.artist + "<br />";
  270. }
  271. if ($.cookie("notification") === "true")
  272. songNotify(obj.data.title, obj.data.artist + " " + obj.data.album );
  273. else
  274. $('.top-right').notify({
  275. message:{html: notification},
  276. type: "info",
  277. }).show();
  278. break;
  279. case "mpdhost":
  280. $('#mpdhost').val(obj.data.host);
  281. $('#mpdport').val(obj.data.port);
  282. if(obj.data.passwort_set) {
  283. $('#mpd_pw').attr('placeholder', '*******');
  284. $('#mpd_pw_con').attr('placeholder', '*******');
  285. }
  286. break;
  287. case "error":
  288. $('.top-right').notify({
  289. message:{text: obj.data},
  290. type: "danger",
  291. }).show();
  292. default:
  293. break;
  294. }
  295. }
  296. socket.onclose = function(){
  297. console.log("disconnected");
  298. $('.top-right').notify({
  299. message:{text:"Connection to ympd lost, retrying in 3 seconds "},
  300. type: "danger",
  301. onClose: function () {
  302. webSocketConnect();
  303. }
  304. }).show();
  305. }
  306. } catch(exception) {
  307. alert('<p>Error' + exception);
  308. }
  309. }
  310. function get_appropriate_ws_url()
  311. {
  312. var pcol;
  313. var u = document.URL;
  314. /*
  315. /* We open the websocket encrypted if this page came on an
  316. /* https:// url itself, otherwise unencrypted
  317. /*/
  318. if (u.substring(0, 5) == "https") {
  319. pcol = "wss://";
  320. u = u.substr(8);
  321. } else {
  322. pcol = "ws://";
  323. if (u.substring(0, 4) == "http")
  324. u = u.substr(7);
  325. }
  326. u = u.split('/');
  327. return pcol + u[0];
  328. }
  329. var updateVolumeIcon = function(volume)
  330. {
  331. $("#volume-icon").removeClass("glyphicon-volume-off");
  332. $("#volume-icon").removeClass("glyphicon-volume-up");
  333. $("#volume-icon").removeClass("glyphicon-volume-down");
  334. if(volume == 0) {
  335. $("#volume-icon").addClass("glyphicon-volume-off");
  336. } else if (volume < 50) {
  337. $("#volume-icon").addClass("glyphicon-volume-down");
  338. } else {
  339. $("#volume-icon").addClass("glyphicon-volume-up");
  340. }
  341. }
  342. var updatePlayIcon = function(state)
  343. {
  344. $("#play-icon").removeClass("glyphicon-play")
  345. .removeClass("glyphicon-pause");
  346. $('#track-icon').removeClass("glyphicon-play")
  347. .removeClass("glyphicon-pause")
  348. .removeClass("glyphicon-stop");
  349. if(state == 1) { // stop
  350. $("#play-icon").addClass("glyphicon-play");
  351. $('#track-icon').addClass("glyphicon-stop");
  352. } else if(state == 2) { // pause
  353. $("#play-icon").addClass("glyphicon-pause");
  354. $('#track-icon').addClass("glyphicon-play");
  355. } else { // play
  356. $("#play-icon").addClass("glyphicon-play");
  357. $('#track-icon').addClass("glyphicon-pause");
  358. }
  359. }
  360. function updateDB() {
  361. socket.send('MPD_API_UPDATE_DB');
  362. $('.top-right').notify({
  363. message:{text:"Updating MPD Database... "}
  364. }).show();
  365. }
  366. function clickPlay() {
  367. if($('#track-icon').hasClass('glyphicon-stop'))
  368. socket.send('MPD_API_SET_PLAY');
  369. else
  370. socket.send('MPD_API_SET_PAUSE');
  371. }
  372. function basename(path) {
  373. return path.split('/').reverse()[0];
  374. }
  375. $('#btnrandom').on('click', function (e) {
  376. socket.send("MPD_API_TOGGLE_RANDOM," + ($(this).hasClass('active') ? 0 : 1));
  377. });
  378. $('#btnconsume').on('click', function (e) {
  379. socket.send("MPD_API_TOGGLE_CONSUME," + ($(this).hasClass('active') ? 0 : 1));
  380. });
  381. $('#btnsingle').on('click', function (e) {
  382. socket.send("MPD_API_TOGGLE_SINGLE," + ($(this).hasClass('active') ? 0 : 1));
  383. });
  384. $('#btnrepeat').on('click', function (e) {
  385. socket.send("MPD_API_TOGGLE_REPEAT," + ($(this).hasClass('active') ? 0 : 1));
  386. });
  387. $('#btnnotify').on('click', function (e) {
  388. if($.cookie("notification") === "true")
  389. $.cookie("notification", false);
  390. else {
  391. window.webkitNotifications.requestPermission();
  392. if (window.webkitNotifications.checkPermission() == 0)
  393. {
  394. $.cookie("notification", true);
  395. $('btnnotify').addClass("active");
  396. }
  397. }
  398. });
  399. function getHost() {
  400. socket.send('MPD_API_GET_MPDHOST');
  401. function onEnter(event) {
  402. if ( event.which == 13 ) {
  403. confirmSettings();
  404. }
  405. }
  406. $('#mpdhost').keypress(onEnter);
  407. $('#mpdport').keypress(onEnter);
  408. $('#mpd_pw').keypress(onEnter);
  409. $('#mpd_pw_con').keypress(onEnter);
  410. }
  411. $('#search').submit(function () {
  412. app.setLocation("#/search/"+$('#search > div > input').val());
  413. return false;
  414. });
  415. function confirmSettings() {
  416. if($('#mpd_pw').val().length + $('#mpd_pw_con').val().length > 0) {
  417. if ($('#mpd_pw').val() !== $('#mpd_pw_con').val())
  418. {
  419. $('#mpd_pw_con').popover('show');
  420. setTimeout(function() {
  421. $('#mpd_pw_con').popover('hide');
  422. }, 2000);
  423. return;
  424. } else
  425. socket.send('MPD_API_SET_MPDPASS,'+$('#mpd_pw').val());
  426. }
  427. socket.send('MPD_API_SET_MPDHOST,'+$('#mpdport').val()+','+$('#mpdhost').val());
  428. $('#settings').modal('hide');
  429. }
  430. function notificationsSupported() {
  431. return "webkitNotifications" in window;
  432. }
  433. function songNotify(artist, title) {
  434. if (!notificationsSupported())
  435. return;
  436. if (window.webkitNotifications.checkPermission() == 0) {
  437. var notification = window.webkitNotifications.createNotification("assets/favicon.ico", artist, title);
  438. notification.show();
  439. setTimeout(function(notification) {
  440. notification.cancel();
  441. }, 3000, notification);
  442. } else {
  443. window.webkitNotifications.requestPermission();
  444. }
  445. }