New music service - starting plugin development

I’m not a JS programmer, but are pretty familiar with other languages, so are starting to look at a plugin for getting streamn urls form iheart radio.

I’ve fouind the follow repo that might help with the actually communication with iheart - https://github.com/TooTallNate/iheart, but at teh moment just trying to get my head around create the initial views in volumio.

I’ve updated addToBrowseSources and get an initial menu entry…good start.

So now working on when I click on that entry to get the plugins internal navigation working.

As far as I can tell that’s the handleBrowseUri function.

So I’ve over done the logging here, because the trouble is I can’t see the loogin appearing

iheartrad.prototype.handleBrowseUri = function (curUri) {
	var self = this;

	self.commandRouter.logger.info('iheartrad.handleBrowseUri: ' + curUri);
	var response;

	if (curUri.startsWith('iheartrad')) {
		self.commandRouter.logger.info('iheartrad: found an iheart url');
		if (curUri === 'iheartrad') {
			self.commandRouter.logger.info('iheartrad: Default url: ' + curUri);
			//response = self.getRootContent();
			self.commandRouter.logger.info('iheartrad: Default url: after getRootContent');
		}
		else if (curUri === 'iheartrad/zm') {
			response = self.getRadioContent('zm');

		}
		else {
			self.commandRouter.logger.info('iheartrad: reject');
			response = libQ.reject();
		}
	  }
	  else
		self.commandRouter.logger.info('iheartrad.handleBrowseUri: No uri specififed');

	return response;
};

Heres the result of clicking on teh iheartrad menu item

Oct 02 11:18:47 garage-volumio volumio[23025]: info: CoreCommandRouter::volumioGetState
Oct 02 11:18:47 garage-volumio volumio[23025]: info: Listing playlists
Oct 02 11:18:47 garage-volumio volumio[23025]: info: Listing playlists
Oct 02 11:18:50 garage-volumio volumio[23025]: info: CoreCommandRouter::executeOnPlugin: iheartrad , handleBrowseUri
Oct 02 11:18:50 garage-volumio volumio[23025]: info: iheartrad: iheartrad
Oct 02 11:18:50 garage-volumio volumio[23025]: info: iheartrad: Default url
Oct 02 11:18:50 garage-volumio volumio[23025]: error: Failed to execute browseSource: TypeError: Cannot read property 'fail' of undefined

I can’t see these log entries:

self.commandRouter.logger.info('iheartrad.handleBrowseUri: ' + curUri);
self.commandRouter.logger.info('iheartrad: Default url: ' + curUri);
self.commandRouter.logger.info('iheartrad: Default url: after getRootContent');

When I finish editing my code I’ve been running volumio plugin refresh from inside the code directory (i’m writing and editing on my volumion raspberry pi). I thought that’s all I had to do, but it looks like it’s not loading the updated code, so I need to restart volumio each time? I tend to refresh the plugin page after the update command the activate the plugin.

Any pointers grateful. For reference (it’s ugly…remember when I said I had never coded JS before) repo is here: https://github.com/psyciknz/volumio-plugins/blob/gh-pages/plugins/music_service/iheartrad/index.js

1 Like

Hey @Psycik

It’s probably the value you’re returning for response.

I don’t see the response variable declared in this function. You need to declare it with
var response = value;
or
let response = value;

Declare a “skeleton” response object and just put your information inside it.

I like let. The scope is more what I’m used to. var has a large scope. const is great for constants. Look up some documentation to explain.

O’Reilly has a great book on JavaScript. I highly recommend it. You need to learn Promise chaining, because I’m sure you’re going to need it.

Also, the structure of the response object is very important. Don’t skip any key/value pairs.

When you have the final response, it gets sent to the explodeUri function and then it’s added to the queue. Make sure you have proper fields and all required fields with standard values. You can look at various working plugins to check out what is required.

I did a slight bypass of the explodeUri function because I get 4 tracks as a response. You can see what I did in my handleBrowseUri function. At the moment I’m just picking the first track, but I left the other option in there just in case.

See my index.js here:

Look at handleBrowseUri and it’s response variable and then check out what explodeUri returns. explodeUri is also called when you click a track in the queue.

One more thing: JavaScript is asynchronous (it emulates doing a few things at once but uses one thread – it doesn’t execute code “top to bottom”), so if you haven’t tried that in another language, it will be a learning process. It was for me! It’s actually really cool. Sometimes a job starts and JavaScript just moves on, so keep chaining those Promises or your variables might not have the values you anticipate.

I did the first version of my plugin a couple of years ago and I kept at it until I kludged it together with duct tape. After a break, I started again this past May or so and got a better handle on the flow of things, though I’m still not an expert by any means.

I use VS Code. You can remotely debug your Pi. The setup is a little tricky but I can help you if you want to try it. It’s a really solid editor in my opinion.

Hope that helps!

Hey thanks for such a detailed reply. I’ll look into it tomorrow when in front of a computer.

I’d be keen to use vscode as well, as I use that for python and openHAB . So any instructions would be great.

When developing plugins, I’ve been running volumio plugin refresh. While that seems to fix when I have a syntax error, it doesn’t seem to update anything when I say add a new logging entry. Until I fully restart volumio .

I tried using volumio dev, but that didn’t seem to add much, though I see it talking about a nodemon on 127.0.0.1:9229 - I haven’t found where to set that to 0.0.0.0. To see what it was. Is that what vscode will use?

So I’ve now got

iheartrad.prototype.handleBrowseUri = function (curUri) {
	var self = this;

	self.commandRouter.logger.info('iheartrad.handleBrowseUri: ' + curUri);
	var response = [];
	var defer = libQ.defer();

	var response = {
        navigation: {
            prev: {
                uri: "/iheartrad"
            }, //prev
            lists: [{
                "availableListViews": ["list","grid"],
                "items": []
            }] //lists
        } //navigation
    }; //var response

    var list = response.navigation.lists[0].items;

    if (curUri.startsWith('/iheartrad')) {
		self.commandRouter.logger.info('iheartrad: found an iheart url: ' + curUri);
		if (curUri === '/iheartrad') {
			self.commandRouter.logger.info('iheartrad: Default url: ' + curUri);
			
			list.push({
				service: 'iheartrad',
				type: 'folder',
				title: 'Saved',
				artist: '',
				album: '',
				icon: 'fa fa-folder-open-o',
				url: '/iheartrad/saved'
			});

			list.push({
				service: 'iheartrad',
				type: 'folder',
				title: 'Browse',
				artist: '',
				album: '',
				icon: 'fa fa-folder-open-o',
				url: '/iheartrad/browse'
			});
			self.commandRouter.logger.info('iheartrad: Default url: after getRootContent');
			self.commandRouter.logger.info('iheartrad: list:' + JSON.stringify(list));
		} //if (curUri === 'iheartrad')
		else if (curUri.startsWith('iheartrad/saved')) {
			self.commandRouter.logger.info('iheartrad: Try and search for ZM station id');
			var matches = iHeart.getById('zm-6190');
			if (matches.length > 0) {
				self.commandRouter.logger.info(`iheartrad: matches: ${JSON.stringify(matches)}`);
				const station = matches.stations[0];
				const surl = iHeart.streamURL(station);

				list.push({
					service: 'webradio',
					type: 'station',
					title: 'ZM',
					artist: '',
					album: '',
					icon: 'fa fa-microphone',
					url: 'https://i.mjh.nz/nz/radio.ih.6190'
				});
			} else
				self.commandRouter.logger.info('iheartrad: matches: found nothing');

		} //else if (curUri === 'iheartrad/zm') 
		else {
			self.commandRouter.logger.info('iheartrad: reject');
			response = libQ.reject();
		}
		defer.resolve(response);
    } //if (curUri.startsWith('iheartrad'))
    else
		self.commandRouter.logger.info('iheartrad.handleBrowseUri: No uri specififed: ' + curUri);

    return defer.promise;
};

As my handleBrowseUri. And when I first enter the plugin I see it fire. It generates a folder for saved and for browse. But what I can’t ever see is it do anything when I click on the saved folder. I’ve tried changing types and all sorts, but the handlebrowse never refires…nor anything else it seems.

I didn’t see this post until last night. I thought I was following this thread.

Anyway, take a look at what I changed below. I have not tested this but this might help if you haven’t gotten past this point by now.

I am a little fuzzy about how JavaScript handles copying and aliasing objects. You might be able to just copy the items part of the response object, act on it, and then the result is reflected in the parent response object. I would consult documentation to confirm this.

I’d rather just drill down on the response object and push your objects into that array.

Oh yeah, in case it’s not clear from my comment in your code, you can just call self.logger.info() and self.logger.error() if you haven’t removed that reference from your code created by the plugin code generator.

iheartrad.prototype.handleBrowseUri = function (curUri) {
	var self = this;

    self.logger.info('iheartrad.handleBrowseUri: ' + curUri);
	//self.commandRouter.logger.info('iheartrad.handleBrowseUri: ' + curUri);

	//var response = [];
	var defer = libQ.defer();

	var response = {
        navigation: {
            prev: {
                uri: "/iheartrad"
            }, //prev
            lists: [{
                "availableListViews": ["list","grid"],
                "items": []
            }] //lists
        } //navigation
    }; //var response

    //var list = response.navigation.lists[0].items;

    if (curUri.startsWith('/iheartrad')) {
		self.commandRouter.logger.info('iheartrad: found an iheart url: ' + curUri);
		if (curUri === '/iheartrad') {
			self.commandRouter.logger.info('iheartrad: Default url: ' + curUri);
			
			//list.push({
            response.navigation.lists.items.push({
				service: 'iheartrad',
				type: 'folder',
				title: 'Saved',
				artist: '',
				album: '',
				icon: 'fa fa-folder-open-o',
				url: '/iheartrad/saved'
			});

            //list.push({
            response.navigation.lists.items.push({
				service: 'iheartrad',
				type: 'folder',
				title: 'Browse',
				artist: '',
				album: '',
				icon: 'fa fa-folder-open-o',
				url: '/iheartrad/browse'
			});

			self.commandRouter.logger.info('iheartrad: Default url: after getRootContent');
			self.commandRouter.logger.info('iheartrad: list:' + JSON.stringify(list));
		} //if (curUri === 'iheartrad')
		else if (curUri.startsWith('iheartrad/saved')) {
			self.commandRouter.logger.info('iheartrad: Try and search for ZM station id');
			var matches = iHeart.getById('zm-6190');
			if (matches.length > 0) {
				self.commandRouter.logger.info(`iheartrad: matches: ${JSON.stringify(matches)}`);
				const station = matches.stations[0];
				const surl = iHeart.streamURL(station);

				//list.push({
                response.navigation.lists.items.push({
					service: 'webradio',
					type: 'station',
					title: 'ZM',
					artist: '',
					album: '',
					icon: 'fa fa-microphone',
					url: 'https://i.mjh.nz/nz/radio.ih.6190'
				});
			} else
				self.commandRouter.logger.info('iheartrad: matches: found nothing');

		} //else if (curUri === 'iheartrad/zm') 
		else {
			self.commandRouter.logger.info('iheartrad: reject');
			response = libQ.reject();
		}
		defer.resolve(response);
    } //if (curUri.startsWith('iheartrad'))
    else
		self.commandRouter.logger.info('iheartrad.handleBrowseUri: No uri specififed: ' + curUri);

    return defer.promise;
};

I’ll put together something on debugging with VS Code, but for now you should look at the example for Webstorm here:

Watch the whole thing. You won’t need the ssh forwarding if you use the method I did, but you can try the way he does it also. Zoom in on his command lines to see what he does. Look up VS Code remote debugging for Javascript and there is a lot of information provide by Microsoft.

You need to have Node 8 installed on your machine. That’s pretty old these days; I believe the current version is 14.x? Anyway, at first I installed Debian 8 in a VM with Virtualbox. This was kind of a pain. Then I found that you can install an old Node version locally for your user. I’m sure this can be done in Windows and on a Mac as well.

After that, you just have to get the ports right and attach to the process. When you get that working, crack a few beers or catch a buzz in some fashion. Take a break!

Now you’ll have to learn how to use the debugger. It really helps. The debugger in VS Code is extremely powerful.

Here is the launch.json file for Volumio. Bear in mind that you need to copy the /data and the /volumio directories to the root of your directory here, so I have:

/home/trucker/src/volumio/data
and
/home/trucker/src/volumio/volumio

My plugin is installed at /home/trucker/src/volumio/data/plugins/music_service/pandora

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "attach",
            "name": "Attach remote node: Nodemon",
            "address": "volumio.local",
            "port": 9229,
            // "restart": true,
            "protocol": "inspector",
            "localRoot": "/home/trucker/src/volumio/",
            "remoteRoot": "/"
        }
    ]
}

Okay, I have to take off for now. I hope that helps some!

Here’s a link to installing a specific version of Node.js with nvm:

When I checked the Node.js version used with Volumio earlier this summer, it was 8.11.1. Perhaps the newest Volumio release uses something else. Check with node --version to be sure.

I also installed jshint and its VS Code extension.

Yes, that has happened to me as well. What seems to work for me is to copy the new files (or all of them) to /data/plugins/music_service/iheartrad/ in your case and execute volumio vrestart. If you have a fairly recent Pi (I use a 3B+), the restart is pretty quick, maybe 20 seconds or so. I just wrote a quick shell script, something like:

#!/bin/bash

cp -R ~/yourplugindevelopmentdir/* /data/plugins/music_service/iheartrad/
volumio vrestart
mpc clear # clears songs still playing in mpc
journalctl -f # watch the logs fly by for errors, etc.

Then just chmod +x that thing and fire it when you have changes.

Thanks. I’ll digest this and see if I can apply it.