Extended Tags/Blacklist addons

To discuss development of addons / skins / customization of MediaMonkey.

Moderators: jiri, drakinite, Addon Administrators

Platonius
Posts: 37
Joined: Mon May 25, 2020 9:37 am

Extended Tags/Blacklist addons

Post by Platonius »

Hi all,

Trying to get my head around the way JS/MM5 works with overrides and extendability, but I'm running into some problems.

I want to add an option to the sendTo menu. Which means I need to add an entry in actions.js to window.actions, the sendto entry and the submenu function.

How would I be able to do this? Haven't found a proper way to create an entry in actions_add.js that would use the sendTo menu. Tools and editTags have their own defined _menuItems entry, but that doesn't seem to exist for the sendTo menu.

Anybody can point me in the right direction?
drakinite
Posts: 965
Joined: Tue May 12, 2020 10:06 am
Contact:

Re: Extended Tags/Blacklist addons

Post by drakinite »

The "Send To" is controlled by actions.sendTo, and the submenu is controlled by the function actions.sendTo.submenu.

You can override it with the Override function (documented here: https://www.mediamonkey.com/wiki/Import ... )#Override)

Since the original submenu returns a Promise, you can make your code simpler by using an async function. First you get the original result (which is an array of actions) by taking the result of the $super parameter, then just add to the array with your custom action, and return the modified array. You can choose whatever grouporder and order you want in order to decide where in the submenu your action appears.

Code: Select all

actions.sendTo.override({
    submenu: async function($super, params) {
        var result = await $super(params);
        result.push({
            icon: 'add',
            title: 'My Custom Action',
            execute: function () {
                uitools.toastMessage.show('Hello world!', {disableUndo: true});
            },
            order: 50,
            grouporder: 20,
        });
        return result;
    }
});
Image
Student electrical-computer engineer, web programmer, part-time MediaMonkey developer, full-time MediaMonkey enthusiast
I uploaded many addons to MM's addon page, but not all of those were created by me. "By drakinite, Submitted by drakinite" means I made it on my own time. "By Ventis Media, Inc., Submitted by drakinite" means it may have been made by me or another MediaMonkey developer, so instead of crediting/thanking me, please thank the team. You can still ask me for support on any of our addons.
Platonius
Posts: 37
Joined: Mon May 25, 2020 9:37 am

Re: Extended Tags/Blacklist addons

Post by Platonius »

Thanks! On to the next problem ;)
Platonius
Posts: 37
Joined: Mon May 25, 2020 9:37 am

Re: Extended Tags/Blacklist addons

Post by Platonius »

Aaaaand.... the next problem.

I'm getting a crash:
Uncaught Error: ""setVisibility can be called only on elements in DOM tree!

And since I'm not calling anything to do with visibility, I don't know where it originates from.

Basically, I'm using a playlist called __blacklist__ to store tracks. On start play of a track I reference if it occurs in __blacklist__ and if so skip to the next song. So far so good.

The problem occurs in the menu item to send a track (or selection) to the blacklist. It basically works. But sometimes when right-clicking on a track or selection, I'm getting the above error. Not sure what it clashes with, because if I just right-click now 50 times in a row, it works fine...

I've got code in 2 files. my local.js contains:

Code: Select all

(function () {
    var currState;
    var player = app.player;
    var track;

    var handleBlacklistTrackChange = async function () {
		var testTrack = player.getCurrentTrack(); // cannot use fast, we will need this for async operation
		track = testTrack;
        if( track) {
			var occursInBlacklist = await doesTrackOccurInBlacklist( track );
			if( occursInBlacklist ) {
				app.player.nextAsync(true);
			}
		}
   };

    var onPlaybackState = function (state) {
        if (state === 'trackChanged') {
                handleBlacklistTrackChange();
        };
    };

//
    localListen(player, 'playbackState', onPlaybackState);
    handleBlacklistTrackChange(); // set initial state
})();
And an actions_add.js containing:

Code: Select all

var blacklistName = '_Blacklist_';

var getBlacklist = async function (plst) {
	var playlists = plst.getChildren();
	await playlists.whenLoaded();
	for (var i = 0; i < playlists.count; i++) {
		playlists.locked(function () {
			item = playlists.getValue(i);
		})
		if ( item.title === blacklistName ) {
			blacklistId = item;
			return blacklistId;
		}
	};
};

//This function can be called to check if a given track occurs in the blacklist.
var doesTrackOccurInBlacklist = async function (track) {
	var blacklistId = undefined;
	if( track ) {
		ODS('RW: doesTrackOccurInBlacklist Start: ' + track.artist + ' - ' + track.title + ' ID: ' + track.id);
	}
	blacklistId = await getBlacklist(app.playlists.root);
	if ( blacklistId == undefined ) {
		ODS('RW: doesTrackOccurInBlacklist: No blacklist found');
	}
	var blacklistTracks = blacklistId.getTracklist();
	await blacklistTracks.whenLoaded();
	
	var hasher = {};
	for (var i = 0; i < blacklistTracks.count; i++) {
		blacklistTracks.locked(function () {
			item = blacklistTracks.getValue(i);
		})
		if ( track.id === item.id ) {
			return true;
		}
	}
	return false;
}

window.actions.sendToBlacklist = {
    icon: 'playlist',
    title: _('Send Selection To Blacklist'),
    execute: function () {
		var blacklistId = undefined;
		
		var createBlacklist = async function() {
			ODS('RW: createBlacklist');
			blacklistId = app.playlists.root.newPlaylist();	
			blacklistId.name = blacklistName;//'_Blacklist_';
			blacklistId.isAutoPlaylist = false;
			blacklistId.commitAsync().then(function () {
				blacklistId.isNew = true;
			});
			return blacklistId;
		};
		
		var addToBlacklist = async function() {
			blacklistId = await getBlacklist(app.playlists.root);
			if ( blacklistId == undefined ) {
				createBlacklist();
			}
			if ( blacklistId == undefined ) {
				//Generate error message
				return;
			};
			var list = await uitools.getSelectedTracklist().whenLoaded();
			if (list.count === 0) {
				return;
			}
			blacklistId.addTracksAsync(list);
		}
		
		addToBlacklist();
	}
}

window.actions.sendTo.override({
    submenu: async function($super, params) {
        var result = await $super(params);
        result.push({
 			action: actions.sendToBlacklist,
            order: 50,
            grouporder: 40,
        });
        return result;
    }
});
I can zip and upload the files if needed.
Ludek
Posts: 4945
Joined: Fri Mar 09, 2007 9:00 am

Re: Extended Tags/Blacklist addons

Post by Ludek »

Code: Select all

I'm getting a crash:
Uncaught Error: ""setVisibility can be called only on elements in DOM tree!
This typically happens when you are working with element that is no longer part of DOM tree.
e.g. when a window/component is already destroyed (e.g. view has been switched) and you are not using localListen/localPromise variants (that cancels the listening/promise properly).
Nevertheless everytime the """setVisibility can be called only on elements in DOM tree!" appears then you should normally see the JS callstack, don't you? And with debug builds the crash log is automatically generated to submit (including the callstack).

BTW: In you code instead of

Code: Select all

for (var i = 0; i < playlists.count; i++) {
		playlists.locked(function () {
			item = playlists.getValue(i);
		});
use rather

Code: Select all

playlists.locked(function () {
	for (var i = 0; i < playlists.count; i++) {		
			item = playlists.getValue(i);
})
or even easier:

Code: Select all

listForEach( playlists, (item) => {   });
otherwise you are going through the loop while the list isn't locked for reading and the item at the given index might no longer exists.
Platonius
Posts: 37
Joined: Mon May 25, 2020 9:37 am

Re: Extended Tags/Blacklist addons

Post by Platonius »

listForEach( playlists, (item) => { });
But this would always loop through the whole list, which could create a lot of overhead if you have a big list to loop through.
Nevertheless everytime the "setVisibility can be called only on elements in DOM tree!" appears then you should normally see the JS callstack, don't you? And with debug builds the crash log is automatically generated to submit (including the callstack).
Yes. But I can't get any idea on what's going wrong, as it doesn't seem to directly reference anything I changed. And I've turned developer mode on, so crash logs shouldn't be submitted I think. Since I also believe it has to do with something in my code.

As said - JS isn't a programming language I've worked with before. So things like a promise is new to me. Just trying things and looking at the sample scripts.

Did change some thigns in code, so maybe it's solved now...

Crash log:
https://drive.google.com/file/d/1zSg7Yn ... sp=sharing
Ludek
Posts: 4945
Joined: Fri Mar 09, 2007 9:00 am

Re: Extended Tags/Blacklist addons

Post by Ludek »

Platonius wrote: Wed Jan 05, 2022 4:03 am
listForEach( playlists, (item) => { });
But this would always loop through the whole list, which could create a lot of overhead if you have a big list to loop through.
Yes, but for (var i = 0; i < playlists.count; i++) is also looping through the whole list, isn't it?
You can use both, I wanted just to mention that the whole list should be in read lock (locked) before going through the list it via indexes (to be sure that the indexes are still valid).

Re the crash log: I will have a look...
Platonius
Posts: 37
Joined: Mon May 25, 2020 9:37 am

Re: Extended Tags/Blacklist addons

Post by Platonius »

But I can just use return to exit a loop. I can't exit a foreach.

Also, it may just be that my visibility crash is solved...
drakinite
Posts: 965
Joined: Tue May 12, 2020 10:06 am
Contact:

Re: Extended Tags/Blacklist addons

Post by drakinite »

Valid reason, if you do wish to stop the loop early. Also, if you're looping through several hundred or more, you may wish to do it asynchronously with listAsyncForEach (to avoid blocking the UI with the calculation). But if you aren't experiencing any such stuttering/blocking when doing what you're doing, there's nothing to worry about.

Also, just FYI in case you do end up using it, there was a small typo in Ludek's message:

Code: Select all

playlists.locked(function () {
	for (var i = 0; i < playlists.count; i++) {		
			item = playlists.getValue(i);
	}
})
Missing curly brace
Image
Student electrical-computer engineer, web programmer, part-time MediaMonkey developer, full-time MediaMonkey enthusiast
I uploaded many addons to MM's addon page, but not all of those were created by me. "By drakinite, Submitted by drakinite" means I made it on my own time. "By Ventis Media, Inc., Submitted by drakinite" means it may have been made by me or another MediaMonkey developer, so instead of crediting/thanking me, please thank the team. You can still ask me for support on any of our addons.
Post Reply