Skip to content

Connectors development

yayuyokitano edited this page Oct 1, 2023 · 64 revisions

Introduction

Connectors are the components that are developed for a particular website to talk to the core. They setup the selectors and logic that analyses the webpage (for example a YouTube video) and extracts the relevant meta data to scrobble.

You don't need knowledge of how Chrome extensions work, but it could help if you want to dive deeper in to the core and implement new functionality and change behavior.

Implementation

These can be implemented in as little as a couple of lines:

export {};

Connector.playerSelector = '#player';

Connector.artistTrackSelector = '#player div.rad-tracks > ul > li';

Connector.playButtonSelector = '.jp-play';

See /connectors/radionomy.ts.

However the base connector extensible enough to allow for more advanced behavior such as:

  • Overwriting default metadata getters.
  • Applying custom filters to modify track info.
  • Using custom triggers for state changes.

All connectors are listed in the core/connectors.ts file. If you develop a new connector, you should add it to the list. Read the header in the core/connectors.ts file for further information.

Tips and recommendations

Here is a collection of basic tips that help to implement connectors.

  • Don't trim result in getter functions. It's done in the core.
  • Don't use || null check in getter functions. It's already done in the core.
// Good
Connector.getArtist = () => {
    return Util.getAttrFromSelectors('.artist', 'data');
};


// Bad
Connector.getArtist = () => {
    return Util.getAttrFromSelectors('.artist', 'data') || null;
};
  • Prefer selectors to getter functions.
// Good
Connector.trackSelector = '.track';
// or
Connector.getTrack = () => {
   // We cannot use selectors
   return Util.getAttrFromSelectors('.track', 'data');
};
// Don't use them both in the connector.


// Bad
Connector.getTrack = () => {
   // This is default implementation of BaseConnector function
   // and it is redundant
   return Util.getTextFromSelectors('#track');
};
  • Combine selectors and MetadataFilter object to modify text.
// Good
const filter = MetadataFilter.createFilter({
    track: cleanupTrack
});

Connector.trackSelector = '.track';
Connector.applyFilter(filter);

function cleanupTrack(text: string) {
    // This code is called once if track title is changed
    return text.replace('a', 'b');
}


// Bad
Connector.getTrack = () => {
    const text = Util.getTextFromSelectors('.track');
    // This code is called on every DOM change
    return text.replace('a', 'b');
}
  • Use Connector.artistTrackSelector property or Connector.getArtistTrack function if artist and track info are placed into the single element.
// Good
Connector.artistTrackSelector = '.artist-track';
// or
Connector.getArtistTrack = () => {
    // We cannot use selectors
    const artistTrack = Util.getAttrFromSelectors('.artist-track', 'data');
    // Use `splitArtistTrack` to split ArtistTrack strings
    return Util.splitArtistTrack(artistTrack);
};
// Don't use them both in the connector.


// Bad
Connector.getArtist = () => {
    const artistTrack = Util.getTextFromSelectors('.artist-track');
    return artistTrack && artistTrack.split('-')[0];
};
Connector.getTrack = () => {
    const artistTrack = Util.getTextFromSelectors('.artist-track');
    return artistTrack && artistTrack.split('-')[1];
};
  • Use Util.debugLog function for debug messages in release version:
/*
 * Debug messages will be prefixed, and will be able to be filtered.
 * Feel free to use console.log for debugging, but don't include it
 * in production.
 */
function initConnector() {
    if (isMainPlayer()) {
        Util.debugLog('Use main player');

        initPropsFormMainPlayer();
    } else if (isAlbumPlayer()) {
        Util.debugLog('Use main player');

        initPropsFormAlbumPlayer();
    } else {
        Util.debugLog('Found no player!', 'warn');
    }
}
  • Use either Connector.playButtonSelector or Connector.pauseButtonSelector, but not both:
/*
 * These properties are used to detect the playing state,
 * so using both ones is redundant.
 */

// Good
Connector.playButtonSelector = '.play';
// or
Connector.playButtonSelector = '.pause';


// Bad
Connector.playButtonSelector = '.play';

Connector.playButtonSelector = '.pause';

Metadata filters

The extension uses metadata filters from the metadata-filter module.

A metadata filter is an object that changes song metadata using filter function. Some filter functions are already in filter module; you can create your own filter functions, though.

Also, there are several filter objects available out-of-the-box, e.g. the one which removes unnecessary garbage from YouTube video titles. You can use these objects, as well as filter functions, in your own connectors.

You can apply custom filter by using Connector.applyFilter function:

// A filter that converts song info to upper case
const filter = MetadataFilter.createFilter({all: (text: string) => {
    return text.toUpperCase();
}});

// Use this function to apply the filter to default one
Connector.applyFilter(filter);

You can use this connector as an example of how to use MetadataFilter with custom functions.

You can find documentation of this module here.

Utility functions

The util module contains some useful functions which make the connectors development easier. Check the util.ts file for documentation.

Connector API changes

Connector API changes are listed in this page.

Examples of good connectors

How to name connector file

  1. The connector filename should match the second-level (or lower-level) domain of the service, e.g. for youtube.com correct name is youtube.ts. Don't include top-level domain (TLD) in the filename except in the case specified below.
  2. If two different connectors covers different services with the same domains, but different TLDs, the connector filename should contain both domain and TLD joined by dot symbol, e.g. example.com.ts and example.org.ts.
  3. If the service uses hostname which contains lower-level domains, they should be joined by hyphen symbol in reversed order, e.g. yandex-radio.ts for radio.yandex.ru, spotify-open.ts for open.spotify.com.
  4. If the TLD is a part of the service name (e.g. Listen.moe), include it in the connector filename (listen.moe.ts).
  5. If the company uses the same players (player libraries/engines) for different services (e.g. Wonder, Radiotunes), use the domain of primary service (company name, player library/engine name) for the connector name.

Feel free to name the connector in another way, if current rules dont't fit or reduce readability.

How to set a label for a connector

Labels are displayed in the extension settings where you can toggle connectors.

In almost all cases you should use the label displayed on the website. Some websites use stylized labels (e.g. uppercase), you should also follow this style.

For example:

  • the proper label for app.idagio.com is IDAGIO, but not Idagio
  • the proper label for sndtst.com is Sound Test, but not sndtst

In case when a music service uses different names/name styling on different media resources (website, Twitter, etc.), it should be discussed individually.

How to assign ID for a website

Usually, the id property equals both the connector filename and the second-level domain of the website.

In cases when the connector file named in a different way, you should use second-level domain as id value.

Examples:

  1. YouTube: connector filename is youtube.ts and connector id is youtube (the connector filename and the second-level domain are equal).
  2. Yandex.Radio: the connector filename is yandex-music.ts, but connector id is yandex-radio (the connector is shared between two websites).

Note: Assign a proper ID carefully, as it will be used internally by the extension later. Renaming IDs is not recommended.

Footnote

To sum it up, the basic connector implementation provides selectors/functions to collect track info from a website. Everything else is done by the base connector object and the extension core.

If you don't understand firstly have a look at the existing connectors (/connectors/). Alternatively feel free to create a ticket and ask for help, or a PR with your current progress and there will be people willing to help you out!