-
-
Notifications
You must be signed in to change notification settings - Fork 547
Connectors development
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.
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';
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.
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 orConnector.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
orConnector.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';
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.
The util
module contains some useful functions which make the connectors development easier. Check the util.ts
file for documentation.
Connector API changes are listed in this page.
-
douban-artists.ts
— an example of how to configure selectors for multiple players. -
vk.ts
+vk-dom-inject.ts
— use an injected script to get track info.
- The connector filename should match the second-level (or lower-level) domain of the service, e.g. for
youtube.com
correct name isyoutube.ts
. Don't include top-level domain (TLD) in the filename except in the case specified below. - 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
andexample.org.ts
. - 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
forradio.yandex.ru
,spotify-open.ts
foropen.spotify.com
. - If the TLD is a part of the service name (e.g. Listen.moe), include it in the connector filename (
listen.moe.ts
). - 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.
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 notIdagio
- the proper label for sndtst.com is
Sound Test
, but notsndtst
In case when a music service uses different names/name styling on different media resources (website, Twitter, etc.), it should be discussed individually.
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:
- YouTube: connector filename is
youtube.ts
and connector id isyoutube
(the connector filename and the second-level domain are equal). - Yandex.Radio: the connector filename is
yandex-music.ts
, but connector id isyandex-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.
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!