diff --git a/README.md b/README.md index 8bbb59b..9cca38a 100644 --- a/README.md +++ b/README.md @@ -29,36 +29,27 @@ Turn any device into a complete remote control for your GNU/Linux. ### Other systems -1 - Download this repository and unzip +In a command line : + ```bash +# 1 - Download this repository and unzip unzip linux-remote-control-master.zip -``` -2 - Make a .deb package of the project -```bash +# 2 - Make a .deb package of the project dpkg-deb -b linux-remote-control-master/ lrc.deb -``` -3 - Install .deb package -```bash +# 3 - Install .deb package sudo dpkg -i lrc.deb -``` -4 - Move /opt/lrc-client directory to your device or to directory of your choice (if you prefer you can leave here) -```bash +# 4 - Move /opt/lrc-client directory to your device or to directory of your choice (if you prefer you can leave here) sudo mv /opt/lrc-client your-directory/lrc-client -``` -5 - Start lrc-server -```bash -node /opt/lrc-server/lrc.js -``` -or -```bash -nodejs /opt/lrc-server/lrc.js +# 5 - Start lrc-server +# (depending on the OS, the server can be called `node` or `nodejs`) +node /opt/lrc-server/lrc.js || nodejs /opt/lrc-server/lrc.js ``` -6 - Open the index.html of your-directory/lrc-client in a browser, add your server and have fun +Finally, open the index.html of your-directory/lrc-client in a browser, add your server and have fun ! ## How to install lrc-server @@ -89,7 +80,7 @@ Or open .deb package by graphic interface (double click on the lrc-ffos.deb file ### Configuration -Linux-remote-control will work out-of-the-box in most cases. However, if you wish to change the default settings (for instance, if you wish to use another music player than Rhythmbox), just modify the configuration file in /opt/lrc-server/node_modules/configuration.js +Linux-remote-control will work out-of-the-box in most cases. However, if you wish to change the default settings (for instance, if you wish to use another music player than Rhythmbox), just modify the configuration file in /opt/lrc-server/configuration.js ### Start the server on computer boot @@ -103,7 +94,11 @@ $ crontab -e ### Firewall issues -The default ports for the server are 3000 for HTTP requests and 3001 for WebSockets. You might want to open those ports (at least when you are not on a public network) with tools such as `firewall-config`. +The default port the server is 3000. You might want to open that port (at least when you are not on a public network) with tools such as `firewall-config`, or using the following command : + +```bash +sudo firewall-cmd --add-port=3000/tcp +``` ## Dependences diff --git a/opt/lrc-client/index.html b/opt/lrc-client/index.html index b12380d..ce945bb 100644 --- a/opt/lrc-client/index.html +++ b/opt/lrc-client/index.html @@ -32,7 +32,6 @@
-
@@ -43,8 +42,23 @@
-
-
+
+ Advanced configuration +
+
+
+ +
+
+
+
+ +
+
@@ -362,32 +376,34 @@ +
+
+
+
-
-
-
-
- - + +
-
+
-
00:00
-
00:00
-
- -
- +
00:00
+
00:00
+
+ +
+
- + + + @@ -412,11 +428,11 @@
- - + +
-
+
@@ -460,23 +476,39 @@
- - - + + - + +
-
+
@@ -575,18 +607,19 @@
-
- - - - + + + + + - - - - + + + + + diff --git a/opt/lrc-client/js/connection/bluetooth.js b/opt/lrc-client/js/connection/bluetooth.js new file mode 100644 index 0000000..e69de29 diff --git a/opt/lrc-client/js/connection/connection.js b/opt/lrc-client/js/connection/connection.js new file mode 100644 index 0000000..dc927d8 --- /dev/null +++ b/opt/lrc-client/js/connection/connection.js @@ -0,0 +1,17 @@ +function Connection() {} + +// Functions to implement with each driver +Connection.prototype.send = function(fct, arguments, callback) { throw new Error("Not implemented yet"); }; +Connection.prototype.delete = function() { throw new Error("Not implemented yet"); }; + +/** + * Returns a new Connection_Driver based on the type of + * the server passed in parameter + */ +Connection.factory = function(server) { + var types = { + HTTP: Connection_HTTP, + WebSocket: Connection_WebSocket + }; + return new types[server.type](server); +}; diff --git a/opt/lrc-client/js/connection/http.js b/opt/lrc-client/js/connection/http.js new file mode 100644 index 0000000..8710032 --- /dev/null +++ b/opt/lrc-client/js/connection/http.js @@ -0,0 +1,60 @@ +/** + * Driver to connect to a server via HTTP requests + * (slower and more memory-consuming than WebSocket) + */ +function Connection_HTTP(server) { + Connection.apply(this); + + refresh_rate = server.refresh_rate || 1000; + + this.url = "http://" + server.host + ":" + server.port + '/'; + + var that = this; + this.refresh_interval = setInterval(function() { + that.refresh(); + }, refresh_rate); +} + +// Extends Connection +Connection_HTTP.prototype = new Connection(); + +/** + * Sends a command to the server via HTTP GET + */ +Connection_HTTP.prototype.send = function(fct, arguments, callback) { + callback = callback || function() {}; + arguments = arguments || {}; + + $.get(this.url + fct, arguments).done(callback); +}; + +Connection_HTTP.prototype.delete = function() { + clearInterval(this.refresh_interval); +}; + +/** + * Called every `refresh_rate` miliseconds to fetch useful data from the server, + * including volume, backlight and music infos. + */ +Connection_HTTP.prototype.refresh = function() { + var requests = ['info', 'music_info']; + for(var index in requests) { + // Callback function refreshes every HTML tag that + // has a [data-watch] that is a key in data. + $.get(this.url + requests[index]).done(function(data) { + for(object in data) { + if(data[object] instanceof Object) { + for(key in data[object]) { + var watch_selector = '[data-watch="' + object + '.' + key + '"]'; + $(watch_selector + ':not(.slider ' + watch_selector + ')').html(unescape(data[object][key])); + $('.slider ' + watch_selector).slider("value", unescape(data[object][key])); + } + } else { + var watch_selector = '[data-watch="' + object + '"]'; + $(watch_selector + ':not(.slider ' + watch_selector + ')').html(unescape(data[object])); + $('.slider ' + watch_selector).slider("value", unescape(data[object])); + } + } + }); + } +}; diff --git a/opt/lrc-client/js/connection/websocket.js b/opt/lrc-client/js/connection/websocket.js new file mode 100644 index 0000000..6fd61cc --- /dev/null +++ b/opt/lrc-client/js/connection/websocket.js @@ -0,0 +1,71 @@ +/** + * Driver to connect to a server via a WebSocket + */ +function Connection_WebSocket(server) { + Connection.apply(this); + + window.WebSocket = window.WebSocket || window.MozWebSocket; + this.websocket = new WebSocket("ws://" + server.host + ":" + server.port + '/'); + + // Log about the WebSocket connection + this.websocket.onopen = function () { console.log('WebSocket connection opened'); }; + this.websocket.onclose = function () { + console.log('WebSocket connection closed'); + if(confirm('Connection to the server lost ! Reload app ?')) { + window.location.reload(); + } + }; + this.websocket.onerror = function (error) { console.error('WebSocket error : ' + error); }; + + var that = this; + this.websocket.onmessage = function (message) { + that.refresh(message.data); + }; +} + +// Extends Connection +Connection_WebSocket.prototype = new Connection(); + +Connection_WebSocket.prototype.delete = function() { + this.websocket.onclose = null; + this.websocket.close(); +}; + +/** + * Sends a command to the server via HTTP GET + */ +Connection_WebSocket.prototype.send = function(fct, arguments, callback) { + this.websocket.send(fct + '/' + JSON.stringify(arguments)); + // TODO : Deal with the callback +}; + +/** + * Callback used to refresh the view when a WebSocket message is received + */ +Connection_WebSocket.prototype.refresh = function(data) { + if(!data) { + return; + } + + try { + data = $.parseJSON(data); + } catch(json_error) { + // Ignore parse errors + console.error(json_error); + return + } + + for(object in data) { + if(data[object] instanceof Object) { + for(key in data[object]) { + var watch_selector = '[data-watch="' + object + '.' + key + '"]'; + $(watch_selector + ':not(.slider ' + watch_selector + ')').html(unescape(data[object][key])); + $('.slider ' + watch_selector).slider("value", unescape(data[object][key])); + } + } else { + var watch_selector = '[data-watch="' + object + '"]'; + $(watch_selector + ':not(.slider ' + watch_selector + ')').html(unescape(data[object])); + $('.slider ' + watch_selector).slider("value", unescape(data[object])); + } + } +}; diff --git a/opt/lrc-client/js/custom-commands.js b/opt/lrc-client/js/custom-commands.js index 61745f7..0e5abc2 100644 --- a/opt/lrc-client/js/custom-commands.js +++ b/opt/lrc-client/js/custom-commands.js @@ -31,9 +31,9 @@ Custom_Commands.prototype.refresh_view = function() { // Refresh events $("#custom-commands .custom-commands a[data-command]").click(function() { - $.get( - "http://" + navigator.host + ":" + port + "/lrc", - {cmd: decodeURI($(this).data("command"))}, + connection.send( + "lrc", + { cmd: decodeURI($(this).data("command")) }, function(response) { if(response.stdout !== '') { alert(response.stdout); diff --git a/opt/lrc-client/js/js.js b/opt/lrc-client/js/js.js index 4f9d933..2cecd7f 100644 --- a/opt/lrc-client/js/js.js +++ b/opt/lrc-client/js/js.js @@ -1,28 +1,7 @@ -// Function to convert music time to seconds ___________________________________ -function seconds(time) { - var split = time.split(":"); - if (split.length === 1) { - var seconds = split[0]; - } else if (split.length === 2) { - var seconds = ((split[0] * 60) + parseInt(split[1])); - } else if (split.length === 3) { - var seconds = ((((split[0] * 60) * 60) + parseInt(split[1] * 60)) + parseInt(split[2])); - } - return seconds; -} - -// Function to convert elapsed music time to percent ___________________________ -function percent(elapsed, duration) { - var percent = (seconds(elapsed) / seconds(duration)) * 100; - return percent; -} - // jQuery-UI components ________________________________________________________ $(function() { - $("#video-timeline").slider(); - var video_timeline = $("#video-timeline"); - video_timeline.slider({ + $("#video-timeline").slider({ range: "min", value: 0, min: 0, @@ -31,7 +10,6 @@ $(function() { }); - // Responsive Layout ___________________________________________________________ $(function() { responsive_layout('#main'); @@ -102,43 +80,68 @@ function responsive_layout(selector) { //$(selector + " #now-playing span .name").css("font-size", line / 100 * 60 + "px"); } -navigator.host = ""; -var port = "3000"; -var websocketPort = "3001"; +var connection = null; -//Clear server to back to index +// Clear server to back to index $(function() { $("#clear-server").click(function() { - navigator.host = ""; $("#server-name").html(""); }); }); +/* +TODO : Display server status on server selection + +// Get the server status +function refresh_servers_status() { + var servers = navigator.servers.all(); + for(var index in servers) { + servers[index].index = index; + Connection.server_status(servers[index], function(server, response) { + $('#servers a:eq(' + server.index + ') span') + .text(server.name) + .css({ + fontStyle: 'normal', + opacity: 1 + }); + }, function(server, response) { + $('#servers a:eq(' + server.index + ') span') + .text(server.name + ' (not available)') + .css({ + fontStyle: 'italic', + opacity: 0.5 + }); + }); + } +} + +$(function() { + refresh_servers_status(); + setInterval(refresh_servers_status, 5000); +}); +*/ + // Musics ______________________________________________________________________ // Volume -$(".sound-volume").slider(); -var sound_volume = $(".sound-volume"); -sound_volume.slider({ +$(".sound-volume").slider({ range: "min", value: 0, min: 0, max: 100, - change: function(event, ui) { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd + ui.value + "%"}); + stop: function(event, ui) { + connection.send('lrc', {cmd: $(this).data("base-command").cmd + ui.value + "%"}); } }); // Timeline -$("#music-timeline").slider(); -var music_timeline = $("#music-timeline"); -music_timeline.slider({ +$("#music-timeline").slider({ range: "min", value: 0, min: 0, max: 100, - change: function(event, ui) { - $.get("http://" + navigator.host + ":" + port + "/music", {action: "seek", args: {proportion: ui.value / 100}}); + stop: function(event, ui) { + connection.send('music', {action: "seek", args: {proportion: ui.value / 100}}); } }); @@ -147,49 +150,38 @@ $(function() { $("section#musics .sound-min").click(function() { var volume = $("section#musics .sound-volume").slider("value"); - $("section#musics .sound-volume").slider("value", parseInt(volume - $(this).data("command").step)); + $("section#musics .sound-volume").slider("value", parseInt(volume - $(this).data("step"))); }); $("section#musics .sound-max").click(function() { var volume = $("section#musics .sound-volume").slider("value"); - $("section#musics .sound-volume").slider("value", parseInt($(this).data("command").step) + volume); + $("section#musics .sound-volume").slider("value", parseInt($(this).data("step")) + volume); }); $("#music-controls a").click(function() { - $.get("http://" + navigator.host + ":" + port + "/music", {action: $(this).data("action")}); + connection.send('music', {action: $(this).data("action")}); }); }); // Videos ______________________________________________________________________ $(function() { + // TODO : Volume managing should be done with a jQuery context instead of + // reimplementing the same thing for each specific .sound-volume $("section#videos .sound-min").click(function() { + console.log($(this)); + console.log($(this).data("base-command")); var volume = $("section#videos .sound-volume").slider("value"); - $("section#videos .sound-volume").slider("value", parseInt(volume - $(this).data("command").step)); + $("section#videos .sound-volume").slider("value", parseInt(volume - $(this).data("step"))); }); $("section#videos .sound-max").click(function() { var volume = $("section#videos .sound-volume").slider("value"); - $("section#videos .sound-volume").slider("value", parseInt($(this).data("command").step) + volume); + $("section#videos .sound-volume").slider("value", parseInt($(this).data("step")) + volume); }); - $("#video-controls #video-play-pause").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); - - $("#video-controls *:not(#video-play-pause)").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); }); -// Alt-tab ____________________________________________________________________ -$(function() { - $("#alt-tab a").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); -}); - - // Controls ____________________________________________________________________ $(function() { $("#send-command").click(function() { @@ -200,7 +192,7 @@ $(function() { dangerous = dangerous || command.search(dangerous_commands[index]) != -1; } if (!dangerous) { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: command}).done(function(response) { + connection.send('lrc', {cmd: command}, function(response) { if (response.error) { alert('An error occured'); console.log(response.error); @@ -224,62 +216,36 @@ screen_brightness.slider({ value: 0, min: 0, max: 100, - change: function(event, ui) { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd + ui.value}); + stop: function(event, ui) { + connection.send('lrc', {cmd: $(this).data("base-command").cmd + ui.value}); } }); $(function() { $(".dark-screen").click(function() { var brightness = $("#backlight").slider("value"); - $("#backlight").slider("value", parseInt(brightness - $(this).data("command").step)); + $("#backlight").slider("value", parseInt(brightness - $(this).data("step"))); }); $(".light-screen").click(function() { var brightness = $("#backlight").slider("value"); - $("#backlight").slider("value", parseInt($(this).data("command").step) + brightness); + $("#backlight").slider("value", parseInt($(this).data("step")) + brightness); }); }); +// Commands $(function() { - $("#controls-controls a:not(#reboot, #shutdown)").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); -}); -//Reboot -$(function() { - $("#controls-controls a#reboot").click(function() { - var _confirm = confirm("Reboot. Are you sure ?"); - if (_confirm) { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - } + $("[data-command]:not([data-confirm])").click(function() { + connection.send('lrc', {cmd: $(this).data("command").cmd}); }); -}); -//Shutdown -$(function() { - $("#controls-controls a#shutdown").click(function() { - var _confirm = confirm("Shut Down. Are you sure ?"); - if (_confirm) { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); + // Commands asking for confirmation + $("[data-command][data-confirm]").click(function() { + if (confirm($(this).data("confirm") + ". Are you sure ?")) { + connection.send('lrc', {cmd: $(this).data("command").cmd}); } }); -}); - -// Touchpad ____________________________________________________________________ -$(function() { - $("#mouse-controls a:not(.slideshow)").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); -}); - -// Slideshow ___________________________________________________________________ - -$(function() { - $(".slideshow-controls a:not(.mouse)").click(function() { - $.get("http://" + navigator.host + ":" + port + "/lrc", {cmd: $(this).data("command").cmd}); - }); }); // Custom Commands _____________________________________________________________ @@ -324,9 +290,7 @@ $(function() { command = "xdotool keydown " + char + " keyup " + char; } - $.get("http://" + navigator.host + ":" + port + "/lrc", - {cmd: command} - ); + connection.send('lrc', {cmd: command}); }); }); }); @@ -457,77 +421,3 @@ $(function() { $("#install").show("slide", {direction: "right"}, speed); } }); - -// Ajax ________________________________________________________________________ - -var second, artist, album, title, elapsed, duration, volume, backlight; - - -// My own spiffy ajax wrapper -// Because cache should always be false for this kind of stuff -function pajax(u, cb) { - $.ajax({ - url: "http://" + navigator.host + ":" + port + "/" + u, - dataType: "jsonp", - cache: false, - jsonpCallback: cb}); -} - -// Callback functions for jsonp -function init() { - pajax("info", "setInit"); - setTimeout("pajax('info', 'checkTime')", 1000); -} - -// Set some globals to use in checkTime -function setInit(data) { - artist = unescape(data.artist); - album = unescape(data.album); - title = unescape(data.title); - elapsed = unescape(data.elapsed); - duration = unescape(data.duration); - volume = unescape(data.volume); - backlight = unescape(data.backlight); -} - -// Checks to see if times are different (time has increased by 1 second) -// If not, assumes paused, set state to paused -function checkTime(data) { - if (data != 0) { - second = data.elapsed; - - if (second > elapsed) { - // song is playing - $(function() { - $(".artist").text(artist); - $(".album").text(album); - $(".title").text(title); - $(".elapsed").text(elapsed); - $(".duration").text(duration); - $(".sound-volume").slider("value", volume); - $("#backlight").slider("value", backlight); - - // Music-timeline - $("#music-timeline").slider("value", percent(elapsed, duration)); - - $(".paused").text(""); - $("#music-play-pause").addClass("pause"); - $("#music-play-pause").removeClass("play"); - }); - } - else { - // is paused -// $(function() { -// $(".paused").text("Paused"); -// $("#music-play-pause").addClass("play"); -// $("#music-play-pause").removeClass("pause"); -// }); - } - } - else { - $(".paused").text("An Error Occurred").fadeIn("fast"); - } -} - -// Interval to check and see which song is still playing (if at all) -setInterval("init()", 950); // 1 second diff --git a/opt/lrc-client/js/i18next.js b/opt/lrc-client/js/lib/i18next.js similarity index 100% rename from opt/lrc-client/js/i18next.js rename to opt/lrc-client/js/lib/i18next.js diff --git a/opt/lrc-client/js/jquery-ui-touch-punch.js b/opt/lrc-client/js/lib/jquery-ui-touch-punch.js similarity index 100% rename from opt/lrc-client/js/jquery-ui-touch-punch.js rename to opt/lrc-client/js/lib/jquery-ui-touch-punch.js diff --git a/opt/lrc-client/js/jquery-ui.js b/opt/lrc-client/js/lib/jquery-ui.js similarity index 100% rename from opt/lrc-client/js/jquery-ui.js rename to opt/lrc-client/js/lib/jquery-ui.js diff --git a/opt/lrc-client/js/jquery.js b/opt/lrc-client/js/lib/jquery.js similarity index 100% rename from opt/lrc-client/js/jquery.js rename to opt/lrc-client/js/lib/jquery.js diff --git a/opt/lrc-client/js/local-storage.js b/opt/lrc-client/js/lib/local-storage.js similarity index 100% rename from opt/lrc-client/js/local-storage.js rename to opt/lrc-client/js/lib/local-storage.js diff --git a/opt/lrc-client/js/servers.js b/opt/lrc-client/js/servers.js index 96fb621..91d848b 100644 --- a/opt/lrc-client/js/servers.js +++ b/opt/lrc-client/js/servers.js @@ -18,10 +18,10 @@ Servers.prototype.refresh_view = function() { server.index = index; $("#servers").append('' + server.name + '
'); } - // Refresh server click events $(".server").unbind('click').click(function() { - navigator.host = $(this).data("server").ip; + connection && connection.delete(); + connection = Connection.factory($(this).data("server")); $("#server-name").html($(this).data("server").name); $("#delete-server").data("index", $(this).data("server").index); }); @@ -38,8 +38,6 @@ Servers.prototype.refresh_view = function() { Servers.prototype.rename = function(index, name) { var servers = this.all(); - console.log(servers); - servers[index].name = name; this.save(servers); @@ -49,15 +47,17 @@ Servers.prototype.rename = function(index, name) { $(function() { $("#save").click(function() { navigator.servers.append({ - name: $('#name').val(), - ip: $("#ip").val() + name: $('#add-server input[name="name"]').val(), + host: $('#add-server input[name="ip"]').val(), + type: $('#add-server select[name="connection-type"]').val(), + port: $('#add-server input[name="port"]').val() }); navigator.servers.refresh_view(); }); // Clear fields $("#cancel, #save").click(function() { - $(".fields input").val(""); + $('#add-server input[name="name"]').val(""); }); // Delete Server (removes it from localStorage) @@ -70,7 +70,6 @@ $(function() { }); $("#server-name").blur(function() { - console.log($(this).data('index') + $(this).text()); navigator.servers.rename($("#delete-server").data('index'), $(this).text()); navigator.servers.refresh_view(); }); diff --git a/opt/lrc-client/js/touch.js b/opt/lrc-client/js/touch.js index fd01693..6c8c450 100644 --- a/opt/lrc-client/js/touch.js +++ b/opt/lrc-client/js/touch.js @@ -2,7 +2,7 @@ var CLICK_PREFIX = 'c'; var MOVE_PREFIX = 'm'; var SCROLL_PREFIX = 's'; -var activeTouch, touchCount, scrolling, socket; +var activeTouch, touchCount, scrolling; function startup() { var el = $('#canvas')[0]; @@ -12,14 +12,9 @@ function startup() { activeTouch = null; touchCount = 0; scrolling = false; - socket = null; } function handleTouchstart(evt) { - //check if socket is connected - if (socket === null || socket.readyState != WebSocket.OPEN) { - socket = new WebSocket('ws://' + navigator.host + ':' + websocketPort); - } evt.preventDefault(); if (touchCount == 0) { activeTouch = evt.changedTouches[0]; @@ -39,12 +34,12 @@ function handleTouchmove(evt) { deltaX = evt.changedTouches[0].clientX - activeTouch.clientX; deltaY = evt.changedTouches[0].clientY - activeTouch.clientY; activeTouch = evt.changedTouches[0]; - socket.send(MOVE_PREFIX + deltaX + ';' + deltaY); + connection.send(MOVE_PREFIX, {x: deltaX, y: deltaY}); } else if (touchCount == 2) { deltaY = evt.changedTouches.identifiedTouch(activeTouch.identifier).clientY - activeTouch.clientY; if (deltaY != 0) { activeTouch = evt.changedTouches.identifiedTouch(activeTouch.identifier); - socket.send(SCROLL_PREFIX + deltaY); + connection.send(SCROLL_PREFIX, {s: deltaY}); } } -} \ No newline at end of file +} diff --git a/opt/lrc-client/manifest.webapp.certified b/opt/lrc-client/manifest.webapp.certified new file mode 100644 index 0000000..79b28d2 --- /dev/null +++ b/opt/lrc-client/manifest.webapp.certified @@ -0,0 +1,52 @@ +{ + "version": "0.4", + "name": "Linux Remote Control", + "description": "A complete remote control for GNU/Linux", + "launch_path": "/index.html", + "icons": { + "16": "/img/icons/icon-16.png", + "32": "/img/icons/icon-32.png", + "48": "/img/icons/icon-48.png", + "60": "/img/icons/icon-60.png", + "64": "/img/icons/icon-64.png", + "90": "/img/icons/icon-90.png", + "120": "/img/icons/icon-120.png", + "128": "/img/icons/icon-128.png", + "256": "/img/icons/icon-256.png" + }, + "developer": { + "name": "Raphael Agneli", + "url": "http://www.agneli.com/" + }, + "installs_allowed_from": ["*"], + "locales": { + "en": { + "description": "A complete remote control for GNU/Linux.", + "developer": { + "name": "Raphael Agneli", + "url": "http://www.agneli.com/" + } + }, + "pt": { + "description": "Um controle remoto completo para GNU/Linux.", + "developer": { + "name": "Raphael Agneli", + "url": "http://www.agneli.com/" + } + }, + "es": { + "description": "Un control remoto completo para GNU/Linux.", + "developer": { + "name": "Raphael Agneli", + "url": "http://www.agneli.com/" + } + } + }, + "default_locale": "en", + "orientation": "portrait", + "type": "certified", + "permissions": { + "sms": {}, + "bluetooth": {} + } +} diff --git a/opt/lrc-server/configuration.js b/opt/lrc-server/configuration.js new file mode 100644 index 0000000..0e54121 --- /dev/null +++ b/opt/lrc-server/configuration.js @@ -0,0 +1,16 @@ +exports.config = { + // Available drivers : rhythmbox, moc + music_driver: "rhythmbox", + port: 3000, + // Mouse speed multiplicator + mouse_speed: { + x: 2, + y: 2 + }, + // Available drivers : HTTP, WebSocket + connection_driver: 'WebSocket', + // Milliseconds between each delay ~ Not used with HTTP driver + refresh_delay: 1000, + // Indicates if the client app is installed as a certified app + certified: false +}; diff --git a/opt/lrc-server/lib/cmd.js b/opt/lrc-server/lib/cmd.js new file mode 100644 index 0000000..0df8157 --- /dev/null +++ b/opt/lrc-server/lib/cmd.js @@ -0,0 +1,56 @@ +exports.parse_cmd = function(command) { + command = command.toString().trim(); + + var arguments = ''; + var action = command.substr(0, command.indexOf(' ')) || command; + + if(command.indexOf(' ') !== -1) { + arguments = command.substr(command.indexOf(' ') + 1); + } + + if(action in actions) { + actions[action](arguments.split(' ')); + } else if(action) { + console.log(action + ': unknown command'); + } +} + +var actions = { + help: function(arguments) { + if(arguments.length && arguments[0].length) { + if(arguments[0] in help) { + output(help[arguments[0]]); + } else { + output("No documentation found for " + arguments[0]); + } + return; + } + + output('Available commands are :'); + for(action in actions) { + output('- ' + action); + } + output('Type `help command` for more informations'); + }, + send: function(arguments) { + output('Not implemented yet. Come back later.'); + }, + list: function(arguments) { + output('Not implemented yet. Come back later.'); + } +}; + +var help = { + help: "You are kidding, right ? That's the command you are using.", + send: "Usage : send NUMBER MESSAGE", + list: "Usage : list [NUMBER]\n" + + "Lists all the sms messages received (optionnally from NUMBER only).\n" + + "NUMBER might be a number or a regex to filter phone numbers." +}; + +function output(string) { + strings = string.split('\n'); + for(index in strings) { + console.log('> ' + strings[index]); + } +} diff --git a/opt/lrc-server/lib/connection.js b/opt/lrc-server/lib/connection.js new file mode 100644 index 0000000..ee8e2d6 --- /dev/null +++ b/opt/lrc-server/lib/connection.js @@ -0,0 +1,82 @@ +exports.drivers = {}; + +// HTTP driver +exports.drivers.HTTP = function(servers, actions, config) { + var app = servers.HTTP; + app.all(/\/(.+)/, function(req, res) { + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "X-Requested-With"); + + if(req.params[0] in actions) { + actions[req.params[0]](req.query, function(result) { + res.send(result); + }); + } else { + console.log("Invalid command : " + req.params[0]); + res.send(null); + } + }); + + app.listen(config.port, function () { + console.log('Listening for HTTP requests on port ' + config.port); + }); +}; + +// WebSocket driver +exports.drivers.WebSocket = function(servers, actions, config) { + var WebSocketServer = servers.WebSocket; + + var wss = new WebSocketServer({port: config.port}); + wss.on('connection', function(ws) { + /** + * Messages sent to the websocket server look like : + * action/json_arguments, ex.: + * + * 'info/{"argument1": 123, "argument2": "asc2", ...}' + * + */ + ws.on('message', function(message) { + var action = message.substr(0, message.indexOf('/')); + + try { + var parameters = action && action.length + 1 != message.length ? JSON.parse(message.substr(message.indexOf('/') + 1)) : {}; + } catch (json_error) { + console.log('Bad JSON request : ' + message); + return; + } + + if(action in actions) { + actions[action](parameters, function(result) { + if(result !== null) { + wss.broadcast(result); + } + }); + } else { + wss.broadcast("Error !"); + } + }); + + // Broadcast informations to the clients + setInterval(function() { + var infos = ['info', 'music_info']; + for(var info in infos) { + actions[infos[info]](null, function(result) { + wss.broadcast(JSON.stringify(result)); + }); + } + }, config.refresh_delay); + }); + + wss.broadcast = function(data) { + for(var i in this.clients) { + this.clients[i].send(data); + } + }; + + console.log('Listening for WebSocket requests on port ' + config.port); +}; + +// Bluetooth driver ~ Requires a certified app +exports.drivers.Bluetooth = function(servers, actions, config) { + throw new Error('Not implemented yet'); +}; diff --git a/opt/lrc-server/node_modules/music.js b/opt/lrc-server/lib/music.js similarity index 65% rename from opt/lrc-server/node_modules/music.js rename to opt/lrc-server/lib/music.js index 7ef3e2c..c759c19 100644 --- a/opt/lrc-server/node_modules/music.js +++ b/opt/lrc-server/lib/music.js @@ -6,7 +6,15 @@ exports.drivers.rhythmbox = { infos: "rhythmbox-client --print-playing-format='%ta;%at;%tt;%te;%td;'", parse_infos: function(stdout) { info = stdout.split(";"); - return {artist: escape(info[0]), album: escape(info[1]), title: escape(info[2]), elapsed: info[3], duration: info[4], state: "unknown"}; + return { + artist: escape(info[0]), + album: escape(info[1]), + title: escape(info[2]), + elapsed: info[3], + duration: info[4], + state: "unknown", + play_symbol: escape('') + }; }, toggle_play: "export DISPLAY=:0; xdotool key XF86AudioPlay", stop: "export DISPLAY=:0; xdotool key XF86AudioStop", @@ -21,7 +29,15 @@ exports.drivers.moc = { infos: "mocp -Q '%artist;%album;%song;%cs;%ts;%state;'", parse_infos: function(stdout) { info = stdout.split(";"); - return {artist: escape(info[0]), album: escape(info[1]), title: escape(info[2]), elapsed: parseInt(info[3]), duration: parseInt(info[4]), state: escape(info[5].toLowerCase())}; + return { + artist: escape(info[0]), + album: escape(info[1]), + title: escape(info[2]), + elapsed: parseInt(info[3]), + duration: parseInt(info[4]), + state: escape((info[5] || '').toLowerCase()), + play_symbol: escape(info[5] == 'PAUSE' ? '' : '') + }; }, toggle_play: "mocp -G", stop: "mocp -P", diff --git a/opt/lrc-server/lrc.js b/opt/lrc-server/lrc.js index ad56103..4dde3c0 100644 --- a/opt/lrc-server/lrc.js +++ b/opt/lrc-server/lrc.js @@ -1,124 +1,134 @@ -//CONSTANTS -var CLICK_PREFIX = 'c'; -var MOVE_PREFIX = 'm'; -var SCROLL_PREFIX = 's'; - -var express = require("express"), - app = express(), +// Required stuff +var app = require("express")(), sys = require("sys"), exec = require("child_process").exec, - config = require("configuration.js").config, - music_manager = require("music.js").drivers[config.music_driver], - child; + config = require("./configuration.js").config, + music_manager = require("./lib/music.js").drivers[config.music_driver], + cmd = require("./lib/cmd.js"), + connection = require("./lib/connection.js").drivers[config.connection_driver], + WebSocketServer = require('ws').Server; -// Relative mouse move uses WebSocket -var WebSocketServer = require('ws').Server; -var wss = new WebSocketServer({port: config.websocket_port}); -var values, x, y; -var handleMessage = function(message) { - var prefix = message[0]; - message = message.substr(1); - switch (prefix) { - case CLICK_PREFIX: - //Clicks are not yet sent over websocket - break; - case MOVE_PREFIX: - values = message.split(';'); - x = parseInt(values[0]) * config.mouse_speed.x; - y = parseInt(values[1]) * config.mouse_speed.y; - if (Math.abs(x) > 10) { - if (Math.abs(x) > 20) { - x = x * 2; - } - x = x * 2; - } - if (Math.abs(y) > 10) { - if (Math.abs(y) > 20) { - y = y * 2; - } - y = y * 2; +var actions = { + info: function(parameters, callback) { + exec("amixer sget Master | grep '%]' && xbacklight -get", function(error, stdout, stderr) { + var volume = stdout.split("%]"); + volume = volume[0].split("["); + volume = volume[1]; + + var backlight = stdout.split(/\[o(?:n|ff)\]/); // Matches [on] or [off] + + // Unmute the speakers if necessary + if(stdout.indexOf("[off]") != -1) { + exec("amixer sset Master unmute"); } - exec('xdotool mousemove_relative -- ' + x + ' ' + y, function puts(error, stdout, stderr) {}); - break; - case SCROLL_PREFIX: - var button = message < 0 ? 4 : 5; - exec('xdotool click ' + button, function puts(error, stdout, stderr) {}); - break; - } -} -wss.on('connection', function(ws) { - ws.on('message', handleMessage); -}); -// Route to handle music commands -app.all("/music", function(req, res) { - if ('info' in req.query) { + backlight = backlight[backlight.length-1].trim(); + backlight = backlight.split("."); + backlight = backlight[0] || 0; + + var data = { + volume: volume, + backlight: backlight, + }; + + callback(data); + }); + }, + music_info: function(parameters, callback) { exec(music_manager.infos, function(error, stdout, stderr) { var infos = music_manager.parse_infos(stdout); - res.send(infos); + if(!isNaN(infos.elapsed) && !isNaN(infos.duration)) { + infos["elapsed-formatted"] = Math.floor(infos.elapsed/60) + ':' + (infos.elapsed % 60 < 10 ? '0' : '') + (infos.elapsed % 60); + infos["duration-formatted"] = Math.floor(infos.duration/60) + ':' + (infos.duration % 60 < 10 ? '0' : '') + (infos.duration % 60); + infos["elapsed-percent"] = infos.elapsed/infos.duration*100; + } + callback({music: infos}); }); - } else if ('action' in req.query && req.query.action in music_manager) { - var command = music_manager[req.query.action]; + }, + music: function(parameters, callback) { + if ('action' in parameters && parameters.action in music_manager) { + var command = music_manager[parameters.action]; - if (typeof command == 'string') { - exec(command); - res.send({state: 0}); - } else if (typeof command == 'function') { - command(music_manager, exec, req.query.args || {}); - res.send({state: 0}); + if (typeof command == 'string') { + exec(command); + callback(null) + } else if (typeof command == 'function') { + command(music_manager, exec, parameters.args || {}); + callback(null); + } else { + callback({error: "command not supported by driver " + music_manager.name}); + } } else { - res.send({state: 1, error: "command not supported by driver " + music_manager.name}); + callback({error: "undefined action for driver " + music_manager.name}); } - } else { - res.send({state: 1, error: "undefined action for driver " + music_manager.name}); - } -}); + }, + // Execute an arbitrary command line + lrc: function(parameters, callback) { + var command = parameters.cmd || ''; + exec(command, function(err, stdout, stderr) { + callback({stdout: stdout, error: err, stderr: stderr}); + }); + }, + /* + * Mouse click, short prefix for performence issues + * parameters.b: {1: left click, 2: middle click, 3: right click} + */ + c: function(parameters, callback) { + exec('xdotool click ' + parameters.b, new Function); + callback(null); + }, + // Mouse move + m: function(parameters, callback) { + x = parseInt(parameters.x) * config.mouse_speed.x; + y = parseInt(parameters.y) * config.mouse_speed.y; + if (Math.abs(x) > 10) { + if (Math.abs(x) > 20) { + x = x * 2; + } + x = x * 2; + } + if (Math.abs(y) > 10) { + if (Math.abs(y) > 20) { + y = y * 2; + } + y = y * 2; + } + exec('xdotool mousemove_relative -- ' + x + ' ' + y, new Function); + callback(null); + }, + // Mouse scroll + s: function(parameters, callback) { + var button = parameters.s < 0 ? 4 : 5; + exec('xdotool click ' + button, new Function); + callback(null); + }, +}; -// Handles arbitrary commands sent from lrc-client -app.all("/lrc", function(req, res) { - var command = req.query.cmd; - exec(command, function(err, stdout, stderr) { - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Headers", "X-Requested-With"); - res.send({stdout: stdout, error: err, stderr: stderr}); - }); -}); +// Init the connection with the right server object +var servers = { + HTTP: app, + WebSocket: WebSocketServer, + Bluetooth: null +}; +connection(servers, actions, config); /** - * handles all requests + * Handles all other requests by sending informations about the server */ -app.get(/^\/(.*)/, function(req, res) { - child = exec("amixer sget Master | grep '%]' && xbacklight -get", function(error, stdout, stderr) { - res.header("Content-Type", "text/javascript"); - // error of some sort - if (error !== null) { - res.send("0"); - } else { - // info actually requires us returning something useful - if (req.params[0] == "info") { - var volume = stdout.split("%]"); - volume = volume[0].split("["); - volume = volume[1]; - - var backlight = stdout.split(/\[o(?:n|ff)\]/); // Matches [on] or [off] - - // Unmute the speakers - if(stdout.indexOf("[off]") != -1) { - exec("amixer sset Master unmute"); - } - - backlight = backlight[backlight.length-1].trim(); - backlight = backlight.split("."); - backlight = backlight[0]; - - res.send(req.query.callback + "({'volume':'" + volume + "', 'backlight':'" + backlight + "'})"); - } else { - res.send(req.query.callback + "()"); - } - } - }); +app.get(/.*/, function(req, res) { + res.header("Content-Type", "text/javascript"); + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Headers", "X-Requested-With"); + res.send({status: 'Running', driver: config.connection_driver, port: config.port}); }); -app.listen(config.port, function () { - console.log('Listening on port ' + config.port); -}); +/** + * If certified mode is enabled, we can use the command line as an + * interpreter manage SMS on the client device + */ +if(config.certified) { + console.log('> SMS command line, type `help` for more information.'); + + // Command line listener + process.openStdin().addListener("data", cmd.parse_cmd); +} diff --git a/opt/lrc-server/node_modules/configuration.js b/opt/lrc-server/node_modules/configuration.js deleted file mode 100644 index 42fdeee..0000000 --- a/opt/lrc-server/node_modules/configuration.js +++ /dev/null @@ -1,11 +0,0 @@ -exports.config = { - // Available drivers : rhythmbox, moc - music_driver: "rhythmbox", - port: 3000, - websocket_port: 3001, - // Mouse speed multiplicator - mouse_speed: { - x: 2, - y: 2 - } -};