This readme has been copied from the MediaMonkey developer wiki and has not yet been fully updated with our TypeScript-based changes in MediaMonkey 5.
MediaMonkey 5 is a fully HTML-based desktop application which uses Chromium for rendering. This means that the entire UI is driven by a platform-independent HTML/CSS/JS stack, which is fully accessible to developers and skinners. JS in cooperation with native code drives the non-visual aspects, also fully controllable by developers. This gives unprecedented customization options to create beautiful skins, new or enhanced functionality and great addons in general.
Scripting in MediaMonkey 5 differs greatly from MediaMonkey 4 and below, because it has been designed from the ground-up. It is not difficult to learn, especially if you are familiar with web programming. If you are coming from writing MediaMonkey 4 addons, you will get adjusted in no time at all.
Here is a basic Hello World example for a MediaMonkey 5 addon.
Inside a new folder, create a file named info.json with the essential information about your addon.
{
"title": "My First Addon",
"id": "myFirstAddon",
"description": "Hello world!",
"version": "1.0.0",
"type": "general",
"author": "Jane Doe"
}
Next, create a file named mainwindow_add.js. This code will get added to the end of mainwindow.js, and it will run when MediaMonkey starts.
// Execute when the window is ready
window.whenReady(() => {
uitools.toastMessage.show('Hello world!', {
disableUndo: true
});
});
Then, select your two files and package them into a zip. Make sure that they are in the root of your zip archive and not inside a subfolder. Rename it to myFirstAddon.mmip.
Inside MediaMonkey5, go to Tools > Addons, click Add, and select your addon. After it installs and you reload the window, you will get a popup "toast" message on the bottom of the screen.
The center of all MediaMonkey add-ons is the MMIP. There is nothing special about the file format itself; it is just a ZIP archive with a different filename. You can use any standard method to zip the files, and then just rename it to a .mmip file.
If you wish to automate the process of zipping the MMIP packages, you are in luck.
At the root of an MMIP, there must be a file named info.json. It includes all essential information about the addon. It cannot be in a subfolder.
Here is an example info.json:
{
"title": "My Addon Name",
"id": "myFirstAddon",
"description": "This is my first addon!",
"version": "1.0.0",
"minAppVersion": "5.0.0",
"type": "general",
"author": "Jane Doe",
"icon": "icon.png"
}
Here are the possible attributes info.json can contain:
+-------------------+-----------+------------------------------------+
| Attribute | Required? | Information |
+===================+===========+====================================+
| title | **yes** | This is the name of your addon |
| | | that is visible to the user. |
+-------------------+-----------+------------------------------------+
| id | **yes** | This is the unique ID of your |
| | | addon. It does not have to be |
| | | identical to your title, but it is |
| | | recommended that your addon\'s id |
| | | be similar to its title for |
| | | organizational purposes. The id |
| | | can include the following |
| | | characters: \[a-zA-Z0-9 -\_\'()\]. |
| | | As of 5.0.3, this format will be |
| | | enforced. We recommend sticking to |
| | | an alphanumeric string without |
| | | spaces. |
+-------------------+-----------+------------------------------------+
| description | **yes** | The description of your addon. |
| | | Make sure your description is |
| | | brief, yet conveys the meaning and |
| | | usage of your addon to the user. |
| | | You can create line breaks in the |
| | | description with \\n. |
+-------------------+-----------+------------------------------------+
| version | **yes** | The version number of your addon. |
| | | It must be in the format %d.%d.%d, |
| | | which is three period-separated |
| | | numbers. |
+-------------------+-----------+------------------------------------+
| minAppVersion | no\* | The minimum compatible version of |
| | | MediaMonkey for your addon. Please |
| | | consult |
| | | [Methods_Added_Post_5.0]( |
| | | https://www.mediamonkey.com/wiki/Methods_Added_Post_5.0) |
| | | to view whether your addon is |
| | | incompatible with older versions |
| | | of MediaMonkey. |
+-------------------+-----------+------------------------------------+
| type | no | The category of your addon. The |
| | | existing categories are: general, |
| | | skin, layout, sync, metadata, and |
| | | visualization; but you can create |
| | | your own categories. If |
| | | unspecified, the type defaults to |
| | | general. |
+-------------------+-----------+------------------------------------+
| author | no | The name of the addon\'s author. |
+-------------------+-----------+------------------------------------+
| icon | no | The filename of your addon\'s icon |
| | | image. The image file must be in |
| | | the root of the MMIP. |
+-------------------+-----------+------------------------------------+
| config | no | The filename of your addon\'s |
| | | configuration script. See Adding |
| | | Configurable Settings for more |
| | | info. |
+-------------------+-----------+------------------------------------+
| updateURL | no | Link to a custom URL to check for |
| | | app updates, for addons that are |
| | | self-hosted. See: [Self-hosted |
| | | addon |
| | | s](https://www.mediamonkey.com/wiki/Getting_Started_(Addons)#Self-hosted_addons) |
+-------------------+-----------+------------------------------------+
| installScript | no | The filename of your addon\'s |
| | | install script. The script will |
| | | run once when the addon is being |
| | | installed. `</br>`{=html}The |
| | | variable |
| | | `window.__currentAddonPath` will |
| | | be set, pointing to the filesystem |
| | | folder into which the addon has |
| | | been installed. |
+-------------------+-----------+------------------------------------+
| uninstallScript | no | The filename of your addon\'s |
| | | uninstall script. The script will |
| | | run once when the addon is being |
| | | uninstalled. `</br>`{=html}The |
| | | variable |
| | | `window.__currentAddonPath` will |
| | | be set, pointing to the filesystem |
| | | folder into which the addon has |
| | | been installed. |
+-------------------+-----------+------------------------------------+
| files | no | This only applies to plugins |
| | | (type: \"plugin\"). Contains an |
| | | array of objects, listing plugin |
| | | files to be added. \"src\" is the |
| | | source file path relative to the |
| | | root inside mmip and \"tgt\" is |
| | | the target file path relative to |
| | | the Plugins folder. It can only |
| | | write under the Plugins |
| | | folder.`</br>`{=html}Example: |
| | | |
| | | ``` js |
| | | "files": [{ |
| | | |
| | | "src": "pluginDLL.dll", |
| | | |
| | | "tgt": "pluginDLL.dll" |
| | | }) |
| | | |
| | | ``` |
+-------------------+-----------+------------------------------------+
| showRestartPrompt | no | Default is \"true\". Tells |
| | | MediaMonkey whether to prompt a |
| | | user for a restart. `</br>`{=html} |
| | | Possible values: \"true\", |
| | | \"false\", \"install\", and |
| | | \"uninstall\". `</br>`{=html} If |
| | | \"true\", MM will prompt the user |
| | | for a restart on both install and |
| | | uninstall. `</br>`{=html} If |
| | | \"install\", MM will only prompt |
| | | on install. `</br>`{=html} If |
| | | \"uninstall\", MM will only prompt |
| | | on uninstall. `</br>`{=html} If |
| | | \"false\", neither installing nor |
| | | uninstalling will prompt a |
| | | restart. `</br>`{=html} |
| | | `<u>`{=html}Must be a string if |
| | | provided. Boolean true/false is |
| | | not supported.`</u>`{=html} |
+-------------------+-----------+------------------------------------+
* May be required in the future.
You can choose to include a license file in the root of your addon. If present, when the addon is being installed. the license agreement will be shown to the user, and they will be required to accept the terms before installing.
Supported license file names:
license.txt
license.md
license<2-letter language code>.txt
, such as licenseEN.txt
, licenseFR.txt
, etc.All the HTML/CSS/JS code that handles MM functionality is stored in a tree structure. As a developer, you can add new files, replace existing, or even extend functionality of the existing files. This is achieved by replication of the folder structure in Addons. For example, if a file controls\checkbox.js is present in the Addon, it completely replaces the default MM functionality of a checkbox. Similarly, an _add suffix in a filename extends functionality of an existing file. E.g. dialogs\dlgAbout_add.js can contain code that adds new controls to the layout of the About dialog.
For information on how to load other files & scripts into your code,
please see the sections on requirejs
and localRequirejs
here:
Important_Methods_and_Utilities_(Addons)#requirejs
You can choose to include a script named init.js in the root of your addon. If present, the script will run at startup.
Filetypes that can be overridden:
Filetypes that can be added to:
Filetypes that can be added new:
The versioning of addons must be in the format of three period-separated numbers (for example 0.0.1, 1.2.34, etc.) We recommend using semantic versioning (see https://semver.org).
MediaMonkey has a built-in updater for addons. When the user clicks "Find Updates" in the addons screen, it will check online if there are any updates for addons that are installed. If any are found, the user clicks the download button that appears, then it will download and install the updated addon.
To submit an addon, you must first have an account on the MediaMonkey forum and be signed in.
To add a new version of your addon, simply click Add New Version and follow steps 4-5. Each additional version needs to be approved by a moderator.
To add code to the end of a certain script, create a JS file in its appropriate location, with _add at the end of its name.
For example, if you wish to make an addition to controls/navigationBar.js, make controls/navigationBar_add.js inside your MMIP. You can also entirely replace the file if you name it controls/navigationBar.js inside your MMIP.
For addons hosted on external sites, please provide a field updateURL in info.json to allow MediaMonkey to check for updates to your addon. The server must reply with info on the most recent version of the addon, in either XML or JSON format. More fields can be returned, but will be ignored by current versions of MediaMonkey.
For a working example, see: https://www.happymonkeying.com/update.php?upd=300021 Please note that since the release of that addon, the MinVersionMajor - MaxVersionBuild fields have been replaced with MinAppVersion, as described below.
Example:
<VersionMajor>3</VersionMajor>
<VersionMinor>0</VersionMinor>
<VersionRelease>6</VersionRelease>
<VersionBuild></VersionBuild>
<MinAppVersion>5.0.2</MinAppVersion>
<UpdateURL>http://myserver/myAddon.mmip</UpdateURL>
In JSON, version is provided as a single string instead of the four separate VersionX fields.
Example (equivalent to the XML above):
{
"version": "3.0.6",
"minAppVersion": "5.0.2",
"updateUrl":"http://myserver/myAddon.mmip"
}
(() => { /* Do stuff */ })();
(function() { /* Do stuff */ })();
app.enabledDeveloperMode(true)
during testing. But do not
keep it in your published extension.Array.prototype.func = function () {}
. Details here:
https://www.ventismedia.com/mantis/view.php?id=18145The MediaMonkey interface is controlled largely by actions, which are defined inside actions.js. Most UI elements are tied to actions, and all hotkeys are defined by actions.
Note: Defining custom actions must be done in actions_add.js. Otherwise, MediaMonkey will not properly register the actions.
Actions are defined in the global actions object. Each action contains the following attributes:
+------------+--------------------+-----------+--------------------+
| Attribute | Type | Required? | Information |
+============+====================+===========+====================+
| title | function (string) | **yes** | Function that |
| | | | returns the |
| | | | (string) title of |
| | | | the action. |
+------------+--------------------+-----------+--------------------+
| execute | function | **yes** | Function that runs |
| | | | when the action is |
| | | | executed. |
+------------+--------------------+-----------+--------------------+
| category | function (string) | no | Function that |
| | | | returns the |
| | | | (string) name of |
| | | | the action\'s |
| | | | category. |
| | | | Categories are |
| | | | defined in the |
| | | | global |
| | | | actionCategories |
| | | | object. Default is |
| | | | general. |
+------------+--------------------+-----------+--------------------+
| hotkeyAble | boolean | no | Determines whether |
| | | | the action can be |
| | | | tied to a hotkey. |
| | | | Default is false. |
+------------+--------------------+-----------+--------------------+
| icon | string | no | Icon that shows in |
| | | | menus that contain |
| | | | the action. The |
| | | | icon ids are |
| | | | located in |
| | | | sk |
| | | | in/(iconname).svg. |
+------------+--------------------+-----------+--------------------+
| visible | function (boolean) | no | Determines whether |
| | | | the action will |
| | | | show up in menus. |
| | | | This can be useful |
| | | | for integrating |
| | | | party mode, for |
| | | | example, by |
| | | | disabling your |
| | | | action when party |
| | | | mode is enabled |
| | | | with |
| | | | |
| | | | ` ``vi |
| | | | sible: window.uito |
| | | | ols.getCanEdit``.` |
+------------+--------------------+-----------+--------------------+
Additionally, actions can contain any other custom attributes and methods.
Defining your own custom action in actions_add.js:
actions.myCustomAction = {
title: 'My Custom Action',
hotkeyAble: true,
execute: function () {
messageDlg('This action was created by hotkeyAction script.', 'information', ['btnOK'], {
defaultButton: 'btnOK'
}, undefined);
}
}
Assigning hotkeys is very simple. Hotkeys are handled by the global hotkeys object, and you can register a hotkey with the hotkeys.addHotkey method.
Usage:
hotkeys.addHotkey(hotkey, action, global);
Attribute | Type | Required? | Information |
---|---|---|---|
hotkey |
string |
yes | The hotkey to assign. See Toold > Options > Hotkeys for the right syntax. |
action |
string |
yes | The action (in window.actions) to execute. Case sensitive. |
global |
boolean |
no | Determines whether the hotkey is global (accessible outside the MM window). Default is false. |
Adding a hotkey to your custom action:
hotkeys.addHotkey('Ctrl+Shift+Q', 'myCustomAction');
The addHotkey method automatically handles duplicates and deletions. So if a user deletes the hotkey, you don't have to worry about it showing up again every time the window reloads; and they can manually re-add it.
To see the syntax that MediaMonkey uses for hotkeys, go to the Tools>Options>Hotkeys window and type in your desired hotkey.
MediaMonkey uses a structure for menus that allows infinitely nested sub-menus. The data structure of a menu item is as follows:
menuItem = {
action: {
title: String, // Required
execute: Function, // Usually required
icon: String,
visible: Boolean,
disabled: Boolean
},
order: Number, // Required
grouporder: Number, // Required
submenu: Array
}
The submenu
field, which is optional, contains an array of as many
other menu items as desired. The grouporder
field determines which
group a menu item will belong to, and groups will be automatically
sorted by the order
field. Menu items defined in actions.js are
intentionally given order and grouporder like 10, 20, 30, etc. to allow
addons to insert menu items in between them as desired.
Note: These fields can either be the datatype as described above or
a function which returns the requested data type. At runtime, the global
method resolveToValue
is called, which either returns the primitive
type or calls the provided function. If you analyze the actions in
actions.js, you'll see bits like visible: window.uitools.getCanEdit
.
This is an example of a function which resolves to a boolean.
There are two menu groups that you will most likely want to add items to: The main menu bar, and tracklist menus.
The File, Edit, View, etc. menus are defined in window._menuItems
,
from actions.js. See actions.js or look in the devtools console to see
all the possible submenus to which to add your action. You can only
add to these menus inside actions_add.js.
For example, if you wish to add an action to the "Tools > Edit tags"
submenu, that is under window._menuItems.editTags
:
window._menuItems.editTags.submenu.push({
action: actions.myNewAction,
order: 10,
grouporder: 10
});
Tracklist menus, which may also be called media menus, are the ones that
contain Play now, Play next, Properties, etc. These are defined in
window.menus.tracklistMenuItems
, from controls/trackListView.js. You
should only add to these menus inside controls/trackListView_add.js.
For example, if you wish to add an action below Cut, Copy, and Paste, but above Rename:
window.menus.tracklistMenuItems.push({
action: actions.myNewAction,
order: 31,
grouporder: 40
});
Coming soon
When you wish to save data such as user preferences, the most effective method is to save values in persistent.json. The way you do this is through the app.setValue and app.getValue methods. Both methods require two parameters.
If retrieving a value that is primitive, set the second parameter as undefined.
app.setValue('myExtension_foo', 'bar');
var foo = app.getValue('myExtension_foo', undefined);
When retrieving a value that is an object, you must provide the second parameter as an object. The function will take that object and populate each value if it exists, so you can easily manage default settings this way.
// Saving settings
app.setValue('myExtension_settings', {
option1: 0,
option2: 'electric boogaloo'
});
// Getting settings with hardcoded defaults
var settings = app.getValue('myExtension_settings', {
option1: 0,
option2: 'default'
})
// Passing an empty object works too
var settings = app.getValue('myExtension_settings', {});
This data is automatically saved in persistent.json, which is saved in the user's AppData folder or the Portable subfolder; for non-portable and portable installations, respectively. Persistent.json is only deleted if the user manually deletes it or removes a portable installation. Important: Make sure the keys that you use are unique. Include the addon ID in the key, as demonstrated above, to ensure this.
If you need to manage more data, you can use the database. In MM5, you can access the database through the app.db object. For more information, see: https://www.mediamonkey.com/webhelp/MM5Preview/classes/DB.html.
To execute queries that do not need return values, use app.db.executeQueryAsync:
app.db.executeQueryAsync('CREATE TABLE IF NOT EXISTS myPlugin (_id INTEGER PRIMARY KEY, value TEXT UNIQUE NOT NULL)')
.then(() => {
app.db.executeQueryAsync('INSERT INTO myPlugin [. . .]');
})
.catch(err => {
console.error(err)
});
To execute queries that need return values, use app.db.getQueryResultAsync:
app.db.getQueryResultAsync('SELECT * FROM Albums')
.then(result => {
// do stuff
})
.catch(err => console.error(err));
This method returns a QueryResults object, which is a linked list. See https://www.mediamonkey.com/webhelp/MM5Preview/classes/QueryResults.html.
There are two ways to add configurable options to an addon: via the "config" option in info.json, or modifying the options dialog.
The recommended way of adding configurable settings to an addon, if the settings are not directly related to an existing options panel, is to use the built-in addon configuration options. To do this, you must specify "config" in info.json:
"config": "config.js"
Inside your configuration js file, you must define a window.configInfo object, with two functions: load and save.
Load is executed during page load, and save is executed after "OK" is pressed. Both functions are passed two parameters: pnl and item.
You can opt to add a config HTML as well, by giving it the same base name as your config JS. For example, if it is named config.js, create config.html; if it is named MediaMonkeyRocks.js, create MediaMonkeyRocks.html.
When a configuration script is provided for an addon, the panel will open once the addon is installed. Additionally, a button will appear in its listing to open the configuration panel.
Example usage:
config.html
<div class="uiRows">
<div>
<div data-id="chbOption1" data-control-class="Checkbox" data-tip="Option 1 tooltip">Option 1</div>
</div>
<div>
<div data-id="chbOption2" data-control-class="Checkbox" data-tip="Option 2 tooltip">Option 1</div>
</div>
</div>
config.js
window.configInfo = {
load: function (pnlDiv, addon) {
// Load config with defaults
this.config = app.getValue('myAddon_config', {
option1: true,
option2: false,
});
// Set checkboxes to the configuration settings
var UI = getAllUIElements(pnlDiv);
UI.chbOption1.controlClass.checked = this.config.option1;
UI.chbOption2.controlClass.checked = this.config.option2;
},
save: function (pnlDiv, addon) {
// Save settings according to the checkbox changes
var UI = getAllUIElements(pnlDiv);
this.config.option1 = UI.chbOption1.controlClass.checked;
this.config.option2 = UI.chbOption2.controlClass.checked;
app.setValue('myAddon_config', this.config);
}
}
When modifying the options dialog, you can either add to / modify an existing panel or create your own panel.
Adding to an existing panel can be done with _add JS files. For more context, take a look at dialogs/dlgOptions.js.
For example, if you wish to add to the General Options panel:
(() => {
var options = [
{
title: 'Option 1', // The label that appears on the checkbox/radio button
radiogroup: 'myAddon_RadioOptions', // Self-explanatory
execute: function() {state.RadioOptions = 'option1'} // This function runs whenever the element is clicked
},
{
title: 'Option 2',
radiogroup: 'myAddon_RadioOptions',
execute: function() {state.RadioOptions = 'option2'}
},
{
title: 'My Checkbox',
checkable: true, // Turns it into a checkbox
execute: function() {state.Checkbox = this.checked;}
},
]
var state;
optionPanels.pnl_General.override({
load: function($super, sett, pnlDiv) {
$super(sett, pnlDiv);
console.log('yoyoyoyoyo')
state = app.getValue('myAddon_settings', {
RadioOptions: 'option1',
Checkbox: false
});
// Update checkbox/radiobutton state from settings
if (state.RadioOptions == 'option1') {options[0].checked = true; options[1].checked = false;}
else {options[0].checked = false; options[1].checked = true}
if (state.Checkbox == true) options[2].checked = true;
// Create an HTML menu from the options
divFromSimpleMenu(pnlDiv, options);
},
save: function($super, sett, pnlDiv) {
$super(sett, pnlDiv);
// Save settings
app.setValue('myAddon_settings', state);
}
});
})();
You can define your new panel inside dialogs/dlgOptions, and add it inside dialogs/dlgOptions_add.js.
Coming soon
While most functionality can be reached in MediaMonkey 5 via addons using JS/HTML, it still supports the majority of the Winamp 2.0 API. Winamp plugin support for MediaMonkey 5 is the same as in MediaMonkey 4. See Winamp Plug-ins (MM4) for more details.
To create an MMIP installer for a plugin, you need to include the information about your plugin's file[s] into info.json:
"type": "plugin",
"files": [{
"src": "pluginDLL.dll",
"tgt": "pluginDLL.dll"
}]
It can contain more files. "src" is the source file path relative to the root inside mmip, and "tgt" is target file relative to the Plugins folder inside the MediaMonkey installation. It can write only under the Plugins folder.
If needed, you can also include an installScript and uninstallScript:
"installScript": "install.js",
"uninstallScript": "uninstall.js"
The install script will run during installation and uninstall script will run after uninstallation.
Both will run inside the main window, allowing full access to the UI.
Additionally, the variable window.__currentAddonPath
will be set,
pointing to the filesystem folder into which the addon has been
installed.
Sometimes, you may need to run functions or access properties from the main window in the context of other windows. For example, performing live updates in an options panel. To do this, use the app.dialogs.getMainWindow() method.
If you have a property "x" in the main window, this is how to access it from another window:
app.dialogs.getMainWindow().getValue('x')
If you assign a property or method to "app", you will need to do this to get the version of it that is attached to the main window.
app.dialogs.getMainWindow().getValue('app').myObject.myMethod();
Instead of bitmaps, MediaMonkey uses
SVGs for its
icons. They are stored in the skin/icon
folder. This is because SVGs
can be scaled to any size without appearing pixelated. See Skinning
Guide/Creating Icons for an
introduction on how to create custom icons.
There are three ways to load icons: The data-icon
HTML attribute;
loadIconFast()
; and loadIcon()
.
Whenever HTML is loaded and/or classes are initialized, any and all elements with the attribute "data-icon" will automatically load the SVG files associated with the icon name provided. For details on how it works, check window.initializeControls in mminit.js. See this example:
<div data-control-class="toolbutton" data-icon="music"></div>
This will create a ToolButton control with the icon from music.svg. Using the ToolButton control class is not required, but if you do not add any CSS to limit the size of the icon, it will appear very large.
If you wish to load an icon programmatically, use loadIconFast()
. For
more details, see
loadIconFast.
Only use loadIcon()
if you need to access the raw SVG code before
adding it to the DOM. For more details, see
loadIcon.
More coming soon