Installing themes

Auto-install: Open Harmony's settings, themes tab, and simply copy-paste the raw .css file url in Harmony then hit install. For example

Manual install: Simply put the .css in:

  • %APPDATA%/Harmony/Themes/ on Windows
  • ~/.config/Harmony/Themes/ on Linux
  • ~/Library/Application Support/Harmony/Themes/ on macOS

Building themes

Skills needed: Basic CSS

Harmonys's themes are simple CSS files so very easy to make! You can see examples here.

Below is a list of the selectors most commonly used. Keep in mind that other undocumented selectors are available, you can find them with the developer tools (Cmd/Ctrl + Alt + i)

Available selectors

  • #header the header bar (containing the player)
  • #footer the footer bar (containing buttons)
  • #sidebar the sidebar (containing the playlists)
  • #settings the settings of the app
  • .playlistTitle a playlist in the sidebar
  • .playlistTitle .icon a playlist icon in the sidebar
  • .serviceTitle a service name in the sidebar (above its playlists)
  • #playerControls the div grouping the player buttons
  • #playerControls .icon the icons composing the player buttons
  • #playerTrackTitle the currently playing track title
  • #playerTrackArtist the currently playing track artist
  • #playerTrackCover the currently playing track cover
  • #playerProgress the div containing the buffer & progress bar
  • #playerProgressBar the progress bar
  • #playerBufferBar the buffer bar
  • .rectInput a text input (search included) or select input
  • .rectInput ~ .icon the icons composing the inputs
  • .listFilter when in listview, the headers used to filter
  • .listTrack a track when shown as a list
  • .listTrack:nth-child(even) one of two tracks when shown a list (to make list effect with 2 colors for even & odd items)
  • .coverElement a track (or album) when shown using Coverview
  • .tracksContainer a container of tracks
  • .tabItem a tab in the settings
  • ::-webkit-input-placeholder the text/search input placeholders
  • #specialViewHeader the header showing informations when in special view (ex: when view artist)
  • .icon.icon-play.playing the icon indicating the playlist currently playing


You can also customize the scrollbar like this:

::-webkit-scrollbar {
    width: 7px;
    background: #4d4d4d;
}

::-webkit-scrollbar:hover {
    background: rgba(0, 0, 0, 0.09);
}

::-webkit-scrollbar-thumb {
    background: #666;
    background-clip: padding-box;
    border: 1px solid #444;
    min-height: 10px;
}

::-webkit-scrollbar-thumb:active {
    background: rgba(0,0,0,0.61);
}



Installing plugins

Auto-install: Open Harmony's settings, plugin tab, and simply copy-paste the raw .js file url in Harmony then hit install. For example

Manual install: Simply put the .js in:

  • %APPDATA%/Harmony/Plugins/ on Windows
  • ~/.config/Harmony/Plugins/ on Linux
  • ~/Library/Application Support/Harmony/Plugins/ on macOS

You may have to enable the plugin / connect your account.

Building plugins

Skills needed : Solid JS experience (ES6 preferred)

I really recommend that you inspire yourself and take snippets from the already available plugins source codes. It'll be much faster/easier than writing everything from scratch.

Once your plugin is done, you may consider publishing it as a pull-request to the offical plugin repository, that way, if breaking change is made to the plugin system, I can directly update your plugin and ensure it always work against the latest version of Harmony.


If you need any assistance, or would like to see another method or hook to expand the plugin system, do not hesitate to contact me [email protected]


Also as you'll probably need to debug stuff, you can open the devtools by pressing Cmd/Ctrl+Alt+i. Here you can see output of your console.log and other commands.

I'll just start by giving the structure of a plugin and explaining each properties one by one.

As you can see, a plugin is defined by static properties & public functions ( in the class ExamplePlugin). Functions you put outside this class will be accessible by only your plugin and won't interfere with the rest of the app.


ExamplePlugin.fullName = "Example Plugin"

ExamplePlugin.favsLocation = "example,favs"

ExamplePlugin.isGeneralPlugin = false

ExamplePlugin.scrobbling = true

ExamplePlugin.color = "#EF4500"

ExamplePlugin.settings = {
    active: false,
    specialOption: true
}

ExamplePlugin.contextmenuItems = [
    {
        title: 'Example item',
        fn: () => console.log(tracks)
    }
]

ExamplePlugin.settingsItems = [
    {
        description: 'This is a special option',
        type: 'checkbox',
        id: 'specialOption'
    }
]

ExamplePlugin.loginBtnHtml = `

    <div id='Btn_exampleplugin' class='button login exampleplugin hide' onclick="login('exampleplugin')">Listen with <b>Example Plugin</b></div>
    <div id='LoggedBtn_exampleplugin' class='button login exampleplugin hide' onclick="logout('exampleplugin')">Disconnect</div>
    <span id='error_exampleplugin' class='error hide'>Error, please try to login again</span>

`

ExamplePlugin.loginBtnCss = `
    .exampleplugin {
      background-color: #EF4500;
      background-image: url('data:image/png;base64,xxxxxxxxxxxxxxxx');
    }
`

class ExamplePLugin {

    /*
    * Called when the user click the button to login to the service
    *
    */
    static login (callback) {

    }

    /*
    * Called when the library is refreshed
    *
    */
    static fetchData () {

        return new Promise((resolve, reject) => {

        })
    }

    /*
    * Called when the user wants to like a track from this plugin
    *
    */
    static like (track, callback) {

    }

    /*
    * Called when the user wants to unlikeda track from this plugin
    *
    */
    static unlike (track, callback) {

    }

    /*
    * Called when the user wants to add tracks to a playlist
    *
    */
    static addToPlaylist (tracks, playlistId, callback) {

    }

    /*
    * Called when the user wants to remove tracks to a playlist
    *
    */
    static removeFromPlaylist (tracks, playlistId, callback) {

    }

    /*
    * Called to obtain the stream URL of a specific track
    *
    */
    static getStreamUrl (track, callback) {

    }

    /*
    * Called when the user starts a global search on this plugin
    *
    */
    static searchTracks (query, callback) {

    }

    /*
    * Called when a track is played, if scrobbling for the track's plugin is enabled
    *
    */
    static onTrackPlay (track) {

    }

    /*
    * Called when a track finished playin, if scrobbling for the track's plugin is enabled
    *
    */
    static onTrackEnded (track) {

    }

    /**
    * Called right after the app started, useful to init stuff
    *
    */

    static appStarted () {

    }

}

module.exports = ExamplePlugin

Static properties

.fullName (String)

The complete name of your plugin with all the original ponctuation and characters.

.isGeneralPlugin (Boolean) default: false

Should your plugin be always accessible in every playlists or not ?

As an example, Last.fm is a general plugin while all the others integrated within Harmony are not.

If a plugin is not general, its contextmenuItems will only show on the tracks from this plugin. If a plugin is general, its contextmenuItems will always show, on every tracks from every playlists of the user.

If your plugin is a service bringing tracks to the user, it will almost always be set to false.

.favsLocation (String)

If your plugin is a service bringing tracks to the user, tracks which can be liked, you must provide the playlist containing these liked tracks.

It is used so once a user like a track, it is immediately added to this playlist.

It is simply composed by the service id followed by a comma and the playlist id. For example for SoundCloud: soundcloud,favs

.scrobbling (Boolean) default:

If your plugin bring tracks, should these tracks be scrobbled to services like Last.fm ?

For example, as SoundCloud contains a lot of remixes & indies song

.color (String)

The color (HTML hexadecimal form) representing the best your plugin. Should be as unique as possible. For now only used in the search, but will be used for more stuff in the future.

.settings (Object)

This contains the default value of your plugin's settings. When the app is reset or started for the first time, these values will be used.

To store user preferences, your plugin must use the settings object available from everywhere in the app (and by other plugins). For example, local directories to be added to library by the Local file plugin is accessible by settings.local.paths.

If your plugin needs to be enabled/configured (if it is a service like SoundCloud, Spotify), you must a least define active: false:

Local.settings = {
    active: false
}

You don't need the active property if your plugin only needs to be installed and no configuration/connection is needed.

.settingsItems (Array)

Here you put the items you want to show in the settings of the app.

These items can be of different type:

  • checkbox
  • textbox -> you can then set the placeholder property
  • select -> along with array options

Here is an example taken from the YouTube plugin:

Youtube.settings = {
    active: false,
    quality: 'normal',
    onlyMusicCategory: true
}

Youtube.settingsItems = [
    {
        description: 'Only fetch videos with music category',
        type: 'checkbox',
        id: 'onlyMusicCategory'
    },
    {
        description: 'Playback quality',
        type: 'select',
        id: 'quality',
        options: [{
            value: 'lowest', title: 'Lowest'
        },{
            value: 'normal', title: 'Normal'
        },{
            value: 'best', title: 'Best'
        }]
    }
]

.contextmenuItems (Array)

Items you want to show in the context menu of tracks. If your plugin is general, it will show with every tracks. If not, only with the tracks from your plugin.

.loginBtnHtml (String)

Warning: this will soon be deprecated

The HTML code for your login/connection button.

.loginBtnCss (String)

Warning: this will soon be deprecated

The CSS code for your login/connection button.

Public functions

login

Called when the user click the button to login to the service

Parameters: callback('stopped' or err or null): Return 'stopped' if the user cancelled during the login process.

fetchData

Called when the library is refreshed

Returns a promise (see examples)

like

Called when the user wants to like a track from this plugin

Parameters:

track: a track object/the track to be liked

callback(err or null)

unlike

Called when the user wants to like a track from this plugin

Parameters:

track: a track object/the track to be unliked

callback(err or null)

addToPlaylist

Called when the user wants to add tracks to a playlist

Parameters:

tracks: array of track objects to be added to the playlist

playlistId: the id of the playlist to modify

callback(err or null)

removeFromPlaylist

Called when the user wants to remove tracks from a playlist

Parameters:

tracks: array of track objects to be added to the playlist

playlistId: the id of the playlist to modify

callback(err or null)

getStreamUrl

Called to obtain the stream URL of a specific track

Parameters:

track: a track object

callback(err or null, the stream url, track.id)

searchTracks

Called when the user starts a global search on this plugin

Parameters:

query: the query used to seerch

callback(tracks, query): with tracks an array of tracks corresponding to this query

onTrackPlay

Called when a track is played, if scrobbling for the track's plugin is enabled

Parameters:

track: a track object/the track starting to play

onTrackEnded

Called when a track is played, if scrobbling for the track's plugin is enabled

Parameters:

track: a track object/the track which ended

appStarted

Called when the app is started, once the basic funcionalities have been loaded. Useful if you need to init things.

Miscellaneous

The track object

Tracks inside Harmony are all described the same way as an object with the following structure:

service: -> **Required**  The track origin service eg: 'spotify'
title: -> **Required** The track title
share_url: -> An URL for sharing the track
album: {
    name: -> The name of the album
    id: -> An unique Id for this album
},
trackNumber: -> If inside an album, the position 
artist: {
    name: -> The name of the artist
    id: -> An unique Id for this artist
},
id: -> **Required**  An unique Id for this track
duration: -> The track duration in milliseconds
artwork: -> URL to an artwork

If you are missing some information, assign null to it.

Adding playlists

Every time the user refresh the library, all playlists are removed and the fetchData() (if it exists) for every services is called. In your fetchData() you can add playlists with:

Data.addPlaylist({
    service: -> **Required** The playlist origin service eg: 'spotify',
    editable: --> Boolean, Can we add/remove tracks from this playlist
    title: -> **Required**  The playlist title
    id: -> **Required**  An unique Id for this playlist
    icon: -> If you want an icon different from the default one, set it here
    artwork: -> (Useless for now) URL to an artwork
    tracks: -> An array with the playlist's tracks
})

OAuth authentification

Harmony makes it easy to implement OAuth login using the oauthLogin method. Here is an example of a login function using OAuth:

static login (callback) {
    oauthLogin(oauthUrl, (code) => {
            
        if (!code) return callback('stopped')
    
        auth( code, (err, data) => {
            if (err) return callback(err)
    
            settings.exampleplugins.access_token = data.access_token
    
            callback()
        })
    
    })
}

oauthUrl is the oauth connect url to open

auth() is a function local to your plugin for transforming the returned token into an access token or refresh token.

For now are supported OAuth authentifications returning a token either as a code or token parameter along with error for any errors. If your OAuth returns in a different parameter, get in touch with me.