mpd.js 22 KB

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