From 8e86e660ec95d9fd0f8fa09b52ef51c4ff059160 Mon Sep 17 00:00:00 2001 From: Justin Woody Date: Thu, 14 May 2020 19:07:15 -0400 Subject: [PATCH] GMusicProxy plugin --- README.md | 64 ++ .../music_service/gmusic_proxy/UIConfig.json | 39 ++ .../music_service/gmusic_proxy/config.json | 14 + plugins/music_service/gmusic_proxy/gmusic.svg | 6 + .../gmusic_proxy/i18n/strings_en.json | 6 + plugins/music_service/gmusic_proxy/index.js | 663 ++++++++++++++++++ plugins/music_service/gmusic_proxy/install.sh | 5 + .../music_service/gmusic_proxy/package.json | 25 + .../music_service/gmusic_proxy/uninstall.sh | 4 + 9 files changed, 826 insertions(+) create mode 100644 plugins/music_service/gmusic_proxy/UIConfig.json create mode 100644 plugins/music_service/gmusic_proxy/config.json create mode 100644 plugins/music_service/gmusic_proxy/gmusic.svg create mode 100644 plugins/music_service/gmusic_proxy/i18n/strings_en.json create mode 100644 plugins/music_service/gmusic_proxy/index.js create mode 100644 plugins/music_service/gmusic_proxy/install.sh create mode 100644 plugins/music_service/gmusic_proxy/package.json create mode 100644 plugins/music_service/gmusic_proxy/uninstall.sh diff --git a/README.md b/README.md index ca55a1f3f..73b8b7475 100755 --- a/README.md +++ b/README.md @@ -1 +1,65 @@ # volumio-plugins + +## VSCode setup +To set up Visual Studio Code for debugging plugin development, copy the code from your Volumio2 system to a local workspace: + +```bash +cd [your workspace directory] +# maybe change this to a mount... +scp -r volumio@[volumio IP or Host]:/volumio/* . +mkdir .vscode +# for debugging +echo '{ + "version": "0.2.0", + "configurations": [ { + "type": "node", + "request": "attach", + "name": "Volumio2 Debugger", + "address": "localhost", + "port": "9229", + "localRoot": "${workspaceFolder}/source", + "remoteRoot": "/" + } +] +}' > .vscode/launch.js +``` + +### Running in debug mode + +You may need a few changes to cleanly debug. In the file `./app/index.js` + +```javascript + // look for the logger instantiation + this.logger = new (winston.Logger)({ + transports: [ + new (winston.transports.Console)(), + new (winston.transports.File)({ + filename: logfile, + json: false + }) + ] + }); + // enable winston debugging + this.logger.level = 'debug'; +``` + +In the file `./app/plugins/miscellanea/albumart/index.js`, you may need to modify the forked album art server +to run on a different port: + +```javascript + //Starting server + var albumartServerCmdArray = [self.config.get('port'),self.config.get('folder')]; + var pExecArgv = process.execArgv; + if (typeof v8debug === 'object' || /--debug|--inspect/.test(process.execArgv.join(' '))) { + pExecArgv = ['--inspect-brk=19229']; + } + var albumartServer = fork(__dirname+'/serverStartup.js', albumartServerCmdArray, { execArgv: pExecArgv }); +``` + +Connect to your volumio box/vm and start up in debug mode + +```bash +ssh -L 9229:localhost:9229 volumio@[volumio IP or Host] +sudo systemctl stop volumio.service +node --inspect-brk /volumio/index.js +``` diff --git a/plugins/music_service/gmusic_proxy/UIConfig.json b/plugins/music_service/gmusic_proxy/UIConfig.json new file mode 100644 index 000000000..54068b52a --- /dev/null +++ b/plugins/music_service/gmusic_proxy/UIConfig.json @@ -0,0 +1,39 @@ +{ + "page": { + "label": "TRANSLATE.GMUSIC_CONFIGURATION" + }, + "sections": [ + { + "id": "section_serverinfo", + "element": "section", + "label": "TRANSLATE.SERVER_INFO", + "icon": "fa-plug", + "onSave": {"type":"controller", "endpoint":"music_service/gmusic_proxy", "method":"saveGMusicConfig"}, + "saveButton": { + "label": "TRANSLATE.SAVE", + "data": [ + "address", + "port" + ] + }, + "content": [ + { + "id": "address", + "type":"text", + "element": "input", + "doc": "This is the address of your GMusic Proxy server", + "label": "TRANSLATE.GMUSIC_ADDRESS", + "value": "" + }, + { + "id": "port", + "type":"text", + "element": "input", + "doc": "This is the port of your GMusic Proxy server", + "label": "TRANSLATE.GMUSIC_PORT", + "value": "9999" + } + ] + } + ] +} diff --git a/plugins/music_service/gmusic_proxy/config.json b/plugins/music_service/gmusic_proxy/config.json new file mode 100644 index 000000000..2c5ec7ebc --- /dev/null +++ b/plugins/music_service/gmusic_proxy/config.json @@ -0,0 +1,14 @@ +{ + "enabled": { + "type": "boolean", + "value": false + }, + "address": { + "type": "string", + "value": "" + }, + "port": { + "type": "string", + "value": "9999" + } +} diff --git a/plugins/music_service/gmusic_proxy/gmusic.svg b/plugins/music_service/gmusic_proxy/gmusic.svg new file mode 100644 index 000000000..226b3c17c --- /dev/null +++ b/plugins/music_service/gmusic_proxy/gmusic.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/plugins/music_service/gmusic_proxy/i18n/strings_en.json b/plugins/music_service/gmusic_proxy/i18n/strings_en.json new file mode 100644 index 000000000..c2b16d022 --- /dev/null +++ b/plugins/music_service/gmusic_proxy/i18n/strings_en.json @@ -0,0 +1,6 @@ +{ + "PLUGIN_CONFIGURATION":"Example Plugin Configuration", + "SAVE":"Save", + "GMUSIC_ADDRESS":"GMusic Proxy Address", + "GMUSIC_PORT":"GMusic Proxy Port" +} diff --git a/plugins/music_service/gmusic_proxy/index.js b/plugins/music_service/gmusic_proxy/index.js new file mode 100644 index 000000000..95f849d3c --- /dev/null +++ b/plugins/music_service/gmusic_proxy/index.js @@ -0,0 +1,663 @@ +'use strict'; + +var libQ = require('kew'); +var http = require('http'); +var url = require('url'); +var winston = require('winston'); +var semver = require('compare-versions'); + +var glogger = new (winston.Logger)({ + transports: [new (winston.transports.Console)()] + }); + +/** + * These requires are for our playlist fix + */ +const util = require('util'); +var fs = require('fs-extra'); +const setTimeoutPromise = util.promisify(setTimeout); +/** + * End playlist fix. + */ +const GMP_REQUIRED_VERSION = "2.2.0"; +const GMPTYPE = 'GMusicProxy'; +const GMP = 'gmusic_proxy'; +const GURI = 'gmproxy' +const BROWSEDATA = {name: GMPTYPE, uri: GURI,plugin_type:'music_service',plugin_name:GMP, albumart: '/albumart?sourceicon=music_service/gmusic_proxy/gmusic.svg'}; +const ROOT_ITEMS = [ + new Item('item-no-menu','My Playlists', '', '', 'fa fa-folder-open-o',GURI+':get_all_playlists'), + new Item('item-no-menu','My Stations', '', '', 'fa fa-folder-open-o',GURI+':get_all_stations'), + new Item('item-no-menu','My Library', '', '', 'fa fa-folder-open-o',GURI+':get_collection'), + new Item('item-no-menu','Situations', '', '', 'fa fa-folder-open-o',GURI+':get_situations'), + new Item('item-no-menu','Listen Now - Artists', '', '', 'fa fa-folder-open-o',GURI+':get_listen_now'), + new Item('item-no-menu','Listen Now - Albums', '', '', 'fa fa-folder-open-o',GURI+':get_listen_now:Zm9ybWF0PWpzb24mdHlwZT1hbGJ1bQ=='), + new Item('playlist','Get Top Songs', '', '', 'fa fa-folder-open-o',GURI+':get_top_songs') +]; +const ROOT_MENU = new MenuResponse('/', ROOT_ITEMS); + +const EP_FOLDER = { + itemType: "folder", + itemIcon: "fa fa-folder-open-o" +}; + +const EP_PLAYLIST = { + itemType: "playlist", + itemIcon: "fa fa-folder-open-o" +}; + +const EP_SONG = { + itemType: "song", + itemIcon: "fa fa-music" +}; + +const ENDPOINT_MAP = { + get_song: EP_SONG, + get_playlist: EP_PLAYLIST, + get_station: EP_PLAYLIST, + get_all_playlists: EP_FOLDER, + get_all_stations: EP_FOLDER, + get_collection: EP_PLAYLIST, + get_new_station_by_id: EP_PLAYLIST, + get_listen_now_list: EP_FOLDER +}; + + +module.exports = GMusicProxy; +function GMusicProxy(context) { + var self = this; + this.context = context; + this.commandRouter = this.context.coreCommand; + this.logger = this.context.logger; + glogger = this.context.logger; + this.configManager = this.context.configManager; + setTimeoutPromise(500).then(() => self.fixPlaylist(this.commandRouter)).catch((e) => { + self.logger.debug("GMusicProxy::Error: " + e); + }); +} + +/** + * The method recursively waits until the playlist manager is instantiated and then we + * override the commonAddToPlaylist method so we can deal w/GMP items correctly. +*/ +GMusicProxy.prototype.fixPlaylist = function(commandRouter) { + var self = this; + var playListManager = commandRouter.playListManager; + if (!playListManager || !playListManager.commonAddToPlaylist) { + setTimeoutPromise(500).then((commandRouter) => self.fixPlaylist(commandRouter)).catch((e) => { + self.logger.debug("GMusicProxy:: Error: " + e); + }); + self.logger.info("GMusicProxy:: Playlist Manager still null"); + return; + } + self.logger.info("GMusicProxy:: PlaysList Manager not null, we can try to fix it now"); + self.origPLM = playListManager.commonAddToPlaylist; + playListManager.commonAddToPlaylist = self.commonAddToPlaylist; +} + +GMusicProxy.prototype.commonAddToPlaylist = function(folder, name, service, uri, title) { + var self = this.commandRouter.pluginManager.getPlugin('music_service', GMP); + self.logger.debug("GMusicProxy::commonAddToPlaylist "); + if (service !== GMP) { + return self.origPLM(folder, name, service, uri, title); + } + self.logger.debug("the gmusic override uri:" + uri + " folder:" + folder + " name:" + name + " service:" + service + " title:" + title ); + + var defer = libQ.defer(); + + var filePath = folder + name; + playlistFileExists(filePath) + .then(() => { + self.logger.debug("GMusicProxy::commonAddToPlaylist Exploding items"); + return self.explodeUri(uri) + }) + .then((entries) => { + self.logger.debug("GMusicProxy::commonAddToPlaylist Writing Playlist"); + entries[0].service = GMP; + entries[0].uri = uri; + self.writeJSON(entries, folder, name, defer) + }); + return defer.promise; +} + +function playlistFileExists(filePath) { + var fileDefer=libQ.defer(); + fs.exists(filePath, function (exists) { + if (!exists) { + var playlist = []; + fs.writeJson(filePath, playlist, function (err) { + if (err) { + fileDefer.reject(new Error()); + } else { + fileDefer.resolve(); + } + }) + } else { + fileDefer.resolve(); + } + }); + return fileDefer.promise; +} + +GMusicProxy.prototype.writeJSON = function(entries, folder, name, defer) { + var self = this; + var filePath = folder + name; + self.logger.debug("Reading filepath " + filePath); + fs.readJson(filePath, function (err, data) { + if (err) { + self.logger.warn("ERR "+err); + defer.resolve({success: false}); + } else { + self.logger.debug("Read this data "+data) + if (!data) { + data=[]; + } + var output = data.concat(entries); + + self.saveJSONFile(folder, name, output).then(function(){ + var favourites = self.commandRouter.checkFavourites({uri: path}); + defer.resolve(favourites); + }).fail(function(){ + defer.resolve({success:false}); + }) + } + }); +} + +GMusicProxy.prototype.saveJSONFile = function(localFolder, fileName, data) { + var self=this; + var defer=libQ.defer(); + + if((this.commandRouter.sharedVars.get('myVolumio.cloudDeviceEnabled') === 'true')) + { + return self.commandRouter.executeOnPlugin('system_controller', 'my_volumio', 'saveCloudItem', { + fileName:fileName, + data:data + }); + } else { + fs.writeJson(localFolder + fileName, data, function (err) { + if (err) + defer.reject(new Error()); + else defer.resolve(); + }) + } + + return defer.promise; +} + +GMusicProxy.prototype.onVolumioStart = function() { + var self = this; + var configFile = self.commandRouter.pluginManager.getConfigurationFile(this.context,'config.json'); + this.config = new (require('v-conf'))(); + this.config.loadFile(configFile); + self.logger.debug('GMusicProxy::onVolumioStart'); + + return libQ.resolve(); +}; + +GMusicProxy.prototype.onStart = function() { + var self = this; + self.logger.debug('Starting GMusicProxy plugin'); + self.mpdPlugin = this.commandRouter.pluginManager.getPlugin('music_service','mpd'); + self.addToBrowseSources(); + return libQ.resolve(); +}; + +GMusicProxy.prototype.onStop = function() { + var self = this; + self.commandRouter.volumioRemoveToBrowseSources(BROWSEDATA.name); + return libQ.resolve(); +}; + +GMusicProxy.prototype.onRestart = function() { + return libQ.resolve(); +}; + +// Configuration Methods ----------------------------------------------------------------------------- + +GMusicProxy.prototype.getUIConfig = function() { + var defer = libQ.defer(); + var self = this; + + var lang_code = this.commandRouter.sharedVars.get('language_code'); + + self.commandRouter.i18nJson(__dirname+'/i18n/strings_'+lang_code+'.json', + __dirname+'/i18n/strings_en.json', + __dirname + '/UIConfig.json') + .then(function(uiconf) + { + uiconf.sections[0].content[0].value = self.config.get('address'); + uiconf.sections[0].content[1].value = self.config.get('port'); + defer.resolve(uiconf); + }) + .fail(function() + { + defer.reject(new Error('Failed to load config')); + }); + + return defer.promise; +}; + +GMusicProxy.prototype.getConfigurationFiles = function() { + return ['config.json']; +}; + +GMusicProxy.prototype.setUIConfig = function(data) { + var self = this; + //Perform your installation tasks here +}; + +GMusicProxy.prototype.getConf = function(varName) { + var self = this; + //Perform your installation tasks here +}; + +GMusicProxy.prototype.setConf = function(varName, varValue) { + var self = this; + //Perform your installation tasks here +}; + + +// Playback Controls --------------------------------------------------------------------------------------- +// If your plugin is not a music_sevice don't use this part and delete it + +GMusicProxy.prototype.addToBrowseSources = function () { + this.logger.debug('GMusicProxy::addToBrowseSources'); + this.commandRouter.volumioAddToBrowseSources(BROWSEDATA); +}; + +GMusicProxy.prototype.handleBrowseUri = function(curUri) { + var self = this; + var log = self.logger; + log.debug('GMusicProxy::handleBrowseUri ' + curUri) + if (!curUri.startsWith(GURI)) { + return; + } + var uriTokens = curUri.split(':'); + if (uriTokens.length == 1) { + // root, return default + return libQ.resolve(ROOT_MENU.getEntry()); + } + + return self.fetchMenuEntry(uriTokens, curUri); +}; + +GMusicProxy.prototype.fetchMenuEntry = function(uriTokens, curUri) { + var self = this; + var options = self.uriToOptions(uriTokens); + var defer = libQ.defer(); + self.httpGet(options).then((body) => { + var items = jspfToItems(curUri, body); + var response = new MenuResponse(self.getParentUri(uriTokens),items); + defer.resolve(response.getEntry()); + }).fail((e) => defer.reject(new Error(e))); + return defer.promise; +}; + +GMusicProxy.prototype.addToFavourites = function(meta) { + var self = this; + self.logger.debug('GMusicProxy::addToFavourites ' + JSON.stringify(meta)); + return libQ.resolve({}); +}; + +GMusicProxy.prototype.uriToOptions = function(uriTokens) { + var len = uriTokens.length; + var path; + if (len == 2) { + path = uriTokens[1]; + } else { + path = uriTokens[len-2] + '/' + uriTokens[len-1]; + } + return this.buildOptions(path); +}; + +GMusicProxy.prototype.getParentUri = function(uriTokens) { + if (uriTokens.length == 2) { + return GURI; + } + var copy = [...uriTokens]; + copy.splice(-2,2) + return copy.join(":"); +}; + +// Define a method to clear, add, and play an array of tracks +GMusicProxy.prototype.clearAddPlayTrack = function(track) { + var self = this; + self.logger.debug('GMusicProxy::clearAddPlayTrack'); + + return self.mpdPlugin.sendMpdCommand('stop', []) + .then(() => self.mpdPlugin.sendMpdCommand('clear', [])) + .then(() => { + var streamurl = self.toStreamURL(track); + self.logger.debug("GMusicProxy::clearAddPlayTrack add " + streamurl) + return self.mpdPlugin.sendMpdCommand('add "'+streamurl+'"',[]) + }) + .then(() => { + self.mpdPlugin.clientMpd.on('system', (status) => { + if (typeof status !== 'string') { + status.trackType = GMPTYPE; + } + return self.commandRouter.stateMachine.syncState(status, GMP); + }); + + return self.mpdPlugin.sendMpdCommand('play', []).then(() => this.getAndSyncState()); + + }) + .fail((e) => { return defer.reject(new Error(e));}); + +}; + +GMusicProxy.prototype.prefetch = function (nextTrack) { + var self = this; + self.commandRouter.pushConsoleMessage('[' + Date.now() + '] ' + 'GMusicProxy::prefetch'); + return self.mpdPlugin.sendMpdCommand('add "' + self.toStreamURL(nextTrack) + '"', []) + .then(() => self.mpdPlugin.sendMpdCommand('consume 1', [])); + }; + +GMusicProxy.prototype.toStreamURL = function(track) { + var self = this; + self.logger.debug("GMusicProxy::toStreamURL "+ JSON.stringify(track)) + var uriTokens = track.uri.split(":"); + var lastIdx = uriTokens.length - 1; + return "http://" + self.config.get('address') + ":" + self.config.get('port') + "/" + uriTokens[lastIdx-1] + "/" + uriTokens[lastIdx]; +} + +GMusicProxy.prototype.stop = function () { + var self = this; + self.commandRouter.pushConsoleMessage('[' + Date.now() + '] ' + 'GMusicProxy::stop'); + return self.mpdPlugin.stop().then(() => self.getAndSyncState()); + }; + +GMusicProxy.prototype.pause = function () { + var self = this; + self.commandRouter.pushConsoleMessage('GMusicProxy::pause'); + // TODO don't send 'toggle' if already paused + return self.mpdPlugin.pause().then(() => self.getAndSyncState()); + }; + +GMusicProxy.prototype.resume = function () { + var self = this; + self.commandRouter.pushConsoleMessage('GMusicProxy::resume'); + // TODO don't send 'toggle' if already playing + return self.mpdPlugin.resume().then(() => self.getAndSyncState()); + }; + +GMusicProxy.prototype.seek = function (position) { + var self = this; + self.commandRouter.pushConsoleMessage('GMusicProxy::seek'); + return self.mpdPlugin.seek(position); + }; + +GMusicProxy.prototype.next = function () { + var self = this; + self.commandRouter.pushConsoleMessage('GMusicProxy::next'); + return self.mpdPlugin.sendMpdCommand('next', []).then(() => self.getAndSyncState()); + }; + +GMusicProxy.prototype.previous = function () { + var self = this; + self.commandRouter.pushConsoleMessage('GMusicProxy::previous'); + return self.mpdPlugin.sendMpdCommand('previous', []).then(() => self.getAndSyncState()); + }; + +GMusicProxy.prototype.getAndSyncState = function() { + var self = this; + return self.mpdPlugin.getState().then((state) => { + state.trackType = GMPTYPE; + return self.commandRouter.stateMachine.syncState(state, GMP); + }); +}; + +//Parse state +GMusicProxy.prototype.parseState = function(sState) { + var self = this; + self.logger.debug('GMusicProxy::parseState'); + //Use this method to parse the state and eventually send it with the following function +}; + +// Announce updated State +GMusicProxy.prototype.pushState = function(state) { + var self = this; + self.logger.debug('GMusicProxy::pushState'); + return self.commandRouter.servicePushState(state, GMP); +}; + + +GMusicProxy.prototype.explodeUri = function(uri) { + var self = this; + var defer=libQ.defer(); + glogger.debug("GMusicProxy::explodeUri " + JSON.stringify(uri)); + var uriTokens = uri.split(':'); + // bail... we shouldn't get here for top level menu items + if (uriTokens.length < 4 && uriTokens[uriTokens.length-1] !== 'get_top_songs') { + return defer.resolve; + } + if (uriTokens[uriTokens.length-2] === 'get_song') { + self.fetchSongInfoItem(uriTokens) + .then((item) => { + var response=[item]; + glogger.debug("GMusicProxy::explode callback: " + JSON.stringify(response)) + defer.resolve(response); + }) + .fail((e) => defer.reject(new Error(e))); + } else { + // playlist, etc recurse + self.fetchMenuEntry(uriTokens, uri) + .then((menuResponse) => { + glogger.debug("GMusicProxy::explode fetchMenuEntry:", JSON.stringify(menuResponse)) + var items = menuResponse.navigation.lists[0].items; + items.forEach(i => { delete i.icon; i.type = "track"; }); + defer.resolve(items); + }); + } + // Mandatory: retrieve all info for a given URI + return defer.promise; +}; + +GMusicProxy.prototype.fetchSongInfoItem = function(uriTokens) { + var self = this; + var lastIdx = uriTokens.length-1; + var options = this.buildOptions("get_song_info/" + uriTokens[lastIdx]); + var defer = libQ.defer(); + self.httpGet(options).then((body) => { + var item = self.songInfoToItem(body, uriTokens); + defer.resolve(item); + }).fail((e) => { // retry once + return libQ.delay(1000).then(() => self.httpGet(options)); + }).then((body) => { + var item = self.songInfoToItem(body, uriTokens); + defer.resolve(item); + }).fail((e) => defer.reject(new Error(e))); + return defer.promise; +} + +GMusicProxy.prototype.songInfoToItem = function(body, uriTokens) { + var songMeta = JSON.parse(body); + glogger.debug("songInfoToItem body: " + body) + var item = new Item('track', songMeta.title, songMeta.artist, songMeta.album, null, uriTokens.join(':')); + item.albumart = songMeta.albumArtRef[0].url; + item.duration = Math.trunc(songMeta.durationMillis/1000); + item.samplerate = '44.1 kHz'; + item.bitdepth = '320kbps CBR'; + item.trackType = 'mp3'; + return item; +} + +GMusicProxy.prototype.search = function (query) { + var self=this; + var defer=libQ.defer(); + self.logger.debug("GMusicProxy::search " + query); + + // setup the search type + var searchParams = self.parseSearchType(query.value); + var qString = []; + for(var key in searchParams) { + if (searchParams.hasOwnProperty(key)) { + qString.push(key + '=' + encodeURIComponent(searchParams[key])); + } + } + var path = "get_by_search?" + qString.join('&'); + var options = self.buildOptions(path); + self.httpGet(options).then((body) => { + var items = jspfToItems(GURI+":search:results", body); + var response = new MenuResponse(GURI,items); + var searchResults = response.getEntry().navigation.lists[0]; + searchResults.title = "GMusicProxy returned " + items.length + " results"; + defer.resolve([searchResults]); + }).fail((e) => defer.reject(new Error(e))); + return defer.promise; +}; + +GMusicProxy.prototype.parseSearchType = function (search) { + var typePattern = /(?:(artist|title|type):)/g; + var validTypes = /(matches|album|artist)(.*)?/g; + var searchParts = search.split(typePattern); + var result = { + type: 'matches', + }; + var untaggedText = searchParts[0].trim(); + for (var i = 1; i 0) { + if (result.type === 'artist' && !result.artist) { + result.artist = untaggedText; + } else if (!result.title) { + result.title = untaggedText; + } else { + result.artist = untaggedText; + } + } + return result; +}; + +GMusicProxy.prototype.saveGMusicConfig = function (data) { + let gm_address = data['address'] + let gm_port = parseInt(data['port']); + var options = this.buildOptions('get_version'); + options.host = gm_address; + options.port = gm_port; + var defer=libQ.defer(); + this.httpGet(options).then( + (body) => { + let version = JSON.parse(body)["version"]; + glogger.debug("GMusicProxy::saveGMusicConfig received version " + version); + if (semver.validate(version) && semver(version, GMP_REQUIRED_VERSION)>=0) { + this.config.set('address', gm_address); + this.config.set('port', gm_port); + this.commandRouter.pushToastMessage('success', "Configuration update", 'The configuration has been successfully updated'); + } else { + this.commandRouter.pushToastMessage('error', "Configuration error", 'Connection error. Incorrect parameters or GMusicProxy 1.0.10+ is required for this version of the plugin.'); + } + defer.resolve(); + } + ).fail((e) => { + glogger.error("Failed to contact GMusicProxy " + gm_address + ":" + gm_port); + this.commandRouter.pushToastMessage('error', "Configuration error", 'An error occurred trying to contact the GMusiceProxy'); + defer.reject(e); + }) + + return defer.promise +}; + +GMusicProxy.prototype.buildOptions = function(path) { + var self = this; + var req_headers = { + 'Accept': 'application/json' + }; + var options = { + host: self.config.get('address'), + port: self.config.get('port'), + path: "/" + path, + headers: req_headers + }; + return options; +} + +function Item(type, title, artist, album, icon, uri) { + this.service = GMP; + this.type = type; + this.title = title; + this.name = title; + this.artist = artist; + this.album = album; + this.icon = icon; + this.uri = uri; +} + +function MenuResponse(prevuri, itemArray=[]) { + this.prevuri = prevuri; + this.itemArray = itemArray; +}; + +MenuResponse.prototype.getEntry = function() { + var self = this; + var entry = { + navigation: { + "lists": [ + { + "availableListViews": [ "list", "grid" ], + "items": self.itemArray + } + ], + "prev": { + uri: self.prevuri + } + } + }; + return entry; +} + +function jspfToItems(curUri, body) { + var jspf = JSON.parse(body) + glogger.debug("jspfToItems: " + JSON.stringify(jspf)) + var playlist = jspf.playlist.track + var items = []; + playlist.forEach(entry => { + var entryURL = url.parse(entry.location) + var entryEndpoint = entryURL.pathname.split('/') + var itemMeta = ENDPOINT_MAP[entryEndpoint[1]]; + var itemUri = curUri + entryEndpoint.join(":") + // title album artist + var item = new Item(itemMeta.itemType, entry.title, entry.creator, entry.album, itemMeta.itemIcon, itemUri); + if (entry.image) { + item.albumart = entry.image; + delete item.icon; + } + if (item.type === EP_SONG.itemType) { + item.duration = Math.trunc(entry.duration/1000); + item.samplerate = '44.1 kHz'; + item.bitdepth = '320kbps CBR'; + item.trackType = 'mp3'; + } + items.push(item); + }); + return items; +} + +GMusicProxy.prototype.httpGet = function(options) { + var log = this.logger; + var defer = libQ.defer(); + log.debug(JSON.stringify(options)); + http.get(options, (res) => { + var body = []; + res.on('data', (chunk) => { body.push(chunk); }); + res.on('end', () => { + var response = body.join(''); + defer.resolve(response); + }); + }).on('error', (e) => { + log.error("Got error: " + e.message); + defer.reject(e); + }); + return defer.promise; +} diff --git a/plugins/music_service/gmusic_proxy/install.sh b/plugins/music_service/gmusic_proxy/install.sh new file mode 100644 index 000000000..030e0f74f --- /dev/null +++ b/plugins/music_service/gmusic_proxy/install.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +echo "Installing gmusic proxy Dependencies" + +echo "plugininstallend" diff --git a/plugins/music_service/gmusic_proxy/package.json b/plugins/music_service/gmusic_proxy/package.json new file mode 100644 index 000000000..f361441e4 --- /dev/null +++ b/plugins/music_service/gmusic_proxy/package.json @@ -0,0 +1,25 @@ +{ + "name": "gmusic_proxy", + "version": "1.0.0", + "description": "Google Play Music plugin using gmusicproxy server", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "author": "Justin Woody", + "license": "ISC", + "volumio_info": { + "prettyName": "Google Music Proxy", + "icon": "fa-music", + "plugin_type": "music_service" + }, + "dependencies": { + "winston": "^2.3.1", + "fs-extra": "^0.28.0", + "compare-versions": "^3.0.1", + "kew": "^0.7.0", + "v-conf": "^1.4.0", + "http": "0.0.0", + "util": "^0.11.1" + } +} diff --git a/plugins/music_service/gmusic_proxy/uninstall.sh b/plugins/music_service/gmusic_proxy/uninstall.sh new file mode 100644 index 000000000..46dbd1824 --- /dev/null +++ b/plugins/music_service/gmusic_proxy/uninstall.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +echo "Done" +echo "pluginuninstallend"