<|start_filename|>src/connectors/jiosaavn.js<|end_filename|>
'use strict';

const playerBar = '.c-player';
const artistSelector = `${playerBar} figcaption > p > a`;

Connector.playerSelector = playerBar;

Connector.trackSelector = `${playerBar} figcaption > h4 > a`;

Connector.getArtist = () => {
	const artistElements = document.querySelectorAll(artistSelector);
	return Util.joinArtists(Array.from(artistElements));
};

Connector.pauseButtonSelector = '.o-icon-pause';

Connector.trackArtSelector = `${playerBar} figure > div > a > img`;

Connector.timeInfoSelector = `${playerBar} .c-player__actions > li:nth-child(1)`;


<|start_filename|>src/connectors/gaana.js<|end_filename|>
'use strict';

/**
 * The last track title. Used for detecting new songs.
 * @type {String}
 */
let lastTrackTitle = null;

/**
 * Object that holds information about song.
 * @type {Object}
 */
let songInfo = null;

Connector.playerSelector = '.player';

Connector.trackArtSelector = '.player-artwork img';

Connector.getArtistTrack = () => {
	requestSongInfo();
	return songInfo;
};

Connector.isPlaying = () => {
	if (Util.isArtistTrackEmpty(songInfo)) {
		return false;
	}

	return Util.hasElementClass('.playPause', 'pause');
};

/**
 * Helper functions.
 */

/**
 * Update current song info asynchronously.
 */
async function requestSongInfo() {
	if (isNewSongPlaying()) {
		const relativeUrl = Util.getAttrFromSelectors('#atitle a', 'href');
		const albumInfoUrl = `https://gaana.com${relativeUrl}`;

		try {
			songInfo = await fetchSongInfo(albumInfoUrl);
		} catch (err) {
			Util.debugLog(`Error: ${err}`, 'error');

			resetSongInfo();
		}
	}
}

/**
 * Reset current song info.
 */
function resetSongInfo() {
	songInfo = null;
}

/**
 * Check if song is changed.
 * @return {Boolean} True if new song is playing; false otherwise
 */
function isNewSongPlaying() {
	const track = Util.getTextFromSelectors('#stitle');

	if (lastTrackTitle !== track) {
		lastTrackTitle = track;
		return true;
	}

	return false;
}

/**
 * Load artist page asynchronously and fetch artist name.
 * @param {String} albumInfoUrl Album info URL
 * @return {Promise} Promise that will be resolved with the song info
 */
async function fetchSongInfo(albumInfoUrl) {
	const track = lastTrackTitle;
	let artist = null;

	const response = await fetch(albumInfoUrl);
	const result = await response.text();

	const doc = new DOMParser().parseFromString(result, 'text/html');
	const songElements = doc.querySelectorAll('.s_c > ul > [data-value]');

	for (const song of songElements) {
		const songTitleElement = song.querySelector('.s_title .sng_c');

		if (songTitleElement) {
			const songTitle = songTitleElement.textContent.trim();

			if (songTitle === track) {
				console.log(2222);
				const artists = song.querySelectorAll('.s_artist .sng_c');
				artist = Util.joinArtists(Array.from(artists));
				console.log(artist);
				break;
			}
		}
	}

	return { artist, track };
}


<|start_filename|>src/connectors/allclassical.org.js<|end_filename|>
'use strict';

Connector.playerSelector = '#live';

Connector.trackSelector = '#title';

Connector.artistSelector = '#composer';

Connector.playButtonSelector = '#live-play';

Connector.isScrobblingAllowed = () => document.querySelector('.nowplaying-label').textContent === 'Now Playing';


<|start_filename|>src/connectors/towerrecordsmusic.js<|end_filename|>
'use strict';

/**
 * The last album thumbnail. Used for detecting new thumbnails.
 * @type {String}
 */
let lastAlbumThumbnail = null;

/**
 * Object that holds album title
 * @type {String}
 */
let albumTitle = null;

const filter = MetadataFilter.createFilter(
	MetadataFilter.createFilterSetForFields(
		['artist', 'track', 'albumArtist'],
		katakanaHanToZen
	)
);

Connector.playerSelector = '#audioPlayer';

Connector.artistSelector = '#artistNameAudio';

Connector.getAlbum = () => {
	requestAlbum();
	return albumTitle;
};

Connector.getTrackArt = () => Util.extractUrlFromCssProperty(document.querySelector('#jacketImageAudio').style['background-image']);

Connector.currentTimeSelector = '#currentTimeAudio';

Connector.durationSelector = '#durationTimeAudio';

Connector.trackSelector = '#titleAudio';

Connector.pauseButtonSelector = '.dm-pause';

Connector.applyFilter(filter);

/**
 * Helper functions.
 */

/**
 * Yes, this is extremely janky, and I apologize, but really blame tower for this one
 * To ensure the album can be found:
 * - album thumbnail is fetched from player
 * - the artist page of the artist is fetched
 * - the album list can then be crawled, and compared to find the one where thumbnail matches. Then fetch name there.
 *
 * A very hacky method is used to bypass the limitations of web scrobbler on asynchronous fetching of data
 */

async function fetchAlbumTitle() {

	const albumThumbnailBackground = document.querySelector('#jacketImageAudio').style['background-image'];
	const artistID = document.querySelector('#artistNameAudio').href.split('/').slice(-1)[0];
	const albumListURL = `https://music.tower.jp/artist/album/list/${artistID}`;

	const albumListString = await (await fetch(albumListURL)).text();
	const parser = new DOMParser();
	const albumListDocument = parser.parseFromString(albumListString, 'text/html');

	const albumList = albumListDocument.querySelectorAll('.c-media__image');

	for (const album of albumList) {
		if (album.style['background-image'] === albumThumbnailBackground) {
			return katakanaHanToZen(album.alt); // filter must be manually applied because it doesn't work when applied with metadatafilter
		}
	}

}

/**
 * Update current album title asynchronously.
 */
async function requestAlbum() {
	if (isNewAlbum()) {

		try {
			albumTitle = await fetchAlbumTitle();
		} catch (err) {
			Util.debugLog(`Error: ${err}`, 'error');

			resetAlbumTitle();
		}

	}
}

/**
 * Reset current album title.
 */
function resetAlbumTitle() {
	albumTitle = null;
}

/**
 * Check if album is changed.
 * @return {Boolean} True if new album is playing; false otherwise
 */
function isNewAlbum() {
	const albumThumbnail = document.querySelector('#jacketImageAudio').style['background-image'];

	if (lastAlbumThumbnail !== albumThumbnail) {
		lastAlbumThumbnail = albumThumbnail;
		return true;
	}

	return false;
}

/**
 * Replace half-width katakana with full-width katakana in the text.
 * @param  {String} text String to be filtered
 * @return {String} Filtered string
 */

const katakanaHanToZenMap = { '｡': '。', '｢': '「', '｣': '」', '､': '、', '･': '・', 'ｦ': 'ヲ', 'ｧ': 'ァ', 'ｨ': 'ィ', 'ｩ': 'ゥ', 'ｪ': 'ェ', 'ｫ': 'ォ', 'ｬ': 'ャ', 'ｭ': 'ュ', 'ｮ': 'ョ', 'ｯ': 'ッ', 'ｰ': 'ー', 'ｱ': 'ア', 'ｲ': 'イ', 'ｳ': 'ウ', 'ｴ': 'エ', 'ｵ': 'オ', 'ｶ': 'カ', 'ｷ': 'キ', 'ｸ': 'ク', 'ｹ': 'ケ', 'ｺ': 'コ', 'ｻ': 'サ', 'ｼ': 'シ', 'ｽ': 'ス', 'ｾ': 'セ', 'ｿ': 'ソ', 'ﾀ': 'タ', 'ﾁ': 'チ', 'ﾂ': 'ツ', 'ﾃ': 'テ', 'ﾄ': 'ト', 'ﾅ': 'ナ', 'ﾆ': 'ニ', 'ﾇ': 'ヌ', 'ﾈ': 'ネ', 'ﾉ': 'ノ', 'ﾊ': 'ハ', 'ﾋ': 'ヒ', 'ﾌ': 'フ', 'ﾍ': 'ヘ', 'ﾎ': 'ホ', 'ﾏ': 'マ', 'ﾐ': 'ミ', 'ﾑ': 'ム', 'ﾒ': 'メ', 'ﾓ': 'モ', 'ﾔ': 'ヤ', 'ﾕ': 'ユ', 'ﾖ': 'ヨ', 'ﾗ': 'ラ', 'ﾘ': 'リ', 'ﾙ': 'ル', 'ﾚ': 'レ', 'ﾛ': 'ロ', 'ﾜ': 'ワ', 'ﾝ': 'ン' };

function katakanaHanToZen(text) {

	let textOutput = '';

	for (const character of text) {
		const charCode = character.charCodeAt();

		if (charCode >= 65377 && charCode <= 65437) {
			textOutput += katakanaHanToZenMap[character];
		} else if (charCode === 65438 || charCode === 65439) {
			const lastCharCode = textOutput[textOutput.length - 1].charCodeAt();
			textOutput = textOutput.slice(0, -1) + String.fromCharCode(lastCharCode + charCode - 65437);
		} else {
			textOutput += character;
		}
	}
	return textOutput;
}


<|start_filename|>src/connectors/idagio.js<|end_filename|>
'use strict';

const symphonySelector = '.player-PlayerInfo__infoEl--2jhHY span:nth-child(3) span:first-child';
const commonNameSelector = '.player-PlayerInfo__infoEl--2jhHY span:nth-child(3) span:nth-child(2)';
const directorSelector = '.player-PlayerInfo__recordingInfo--15VMv>span:first-child span';
const trackSelector = '.player-PlayerInfo__infoEl--2jhHY';
const pauseButtonSelector = '.player-PlayerControls__btn--1r-vy:nth-child(2) .util-IconLabel__component--3Uitr span';

Connector.playerSelector = '.player-PlayerBar__bar--2yos_';

Connector.artistSelector = '.player-PlayerInfo__infoEl--2jhHY span:first-child';

Connector.getTrack = getCurrentTrack;

Connector.getAlbum = getCurrentSymphony;

Connector.currentTimeSelector = '.player-PlayerProgress__progress--2F0qB>span';

Connector.durationSelector = '.player-PlayerProgress__timeTotal--3aHlj span';

Connector.isPlaying = () => Util.getTextFromSelectors(pauseButtonSelector) === 'Pause';

Connector.isScrobblingAllowed = () => Util.getTextFromSelectors('.player-PlayerInfo__recordingInfo--15VMv') !== 'Sponsor message';

function getCurrentTrack() {
	/*
	 * Idagio puts composer and piece all in the same div element, so we have to undo this.
	 * First split the tag by dashes. Include spaces to avoid issues with names containing dashes.
	 * The first part is the composer name. We want to exclude this, so we slice it off.
	 * Then, replace the dashes with colons, this makes the tag more in line with what is expected.
	 * Finally, trim as a "just in case" in preparation for the next step. Unnecessary in the cases I've seen, but extremely simple and might prevent something down the line.
	 * Example from https://app.idagio.com/albums/saint-saens-violin-sonata-no-1-cello-sonata-no-1-and-piano-trio-no-2:
	 * "<NAME> – Sonata for Violin and Piano No. 1 in D minor op. 75 R 123 – I. Allegro agitato –"
	 * -> ["<NAME>", "Sonata for Violin and Piano No. 1 in D minor op. 75 R 123", "I. Allegro agitato –"]
	 * -> ["Sonata for Violin and Piano No. 1 in D minor op. 75 R 123", "I. Allegro agitato –"]
	 * -> "Sonata for Violin and Piano No. 1 in D minor op. 75 R 123: I. Allegro agitato –"
	 */
	let track = Util.getTextFromSelectors(trackSelector).split(' – ').slice(1).join(': ').trim();
	/*
	 * Now remove trailing dash if exists.
	 * Example: "Sonata for Violin and Piano No. 1 in D minor op. 75 R 123: I. Allegro agitato –"
	 * -> "Sonata for Violin and Piano No. 1 in D minor op. 75 R 123: I. Allegro agitato " (the space is trimmed later in core)
	 */
	if (track.slice(-1) === '–') {
		track = track.slice(0, -1);
	}
	return track;
}

function getCurrentSymphony() {
	const symphonyShort = Util.getTextFromSelectors(symphonySelector).split(/ in [A-G]| op. [0-9]| KV [0-9]/)[0];
	const commonName = Util.getTextFromSelectors(commonNameSelector) || '';
	const director = removeParenthesis(Util.getTextFromSelectors(directorSelector));
	return `${symphonyShort}${commonName} (${director})`;
}

function removeParenthesis(text) {
	return text.replace(/\s*\(.*?\)\s*/g, '');
}


<|start_filename|>src/connectors/freemusicarchive.js<|end_filename|>
'use strict';

Connector.playerSelector = '.c-player__container';

Connector.artistSelector = '.c-song__artist';

Connector.trackSelector = '.c-song__title';

Connector.timeInfoSelector = '.c-player__time';

Connector.playButtonSelector = '.c-player__control-button--play';


<|start_filename|>src/connectors/lounge.fm.js<|end_filename|>
'use strict';

Connector.playerSelector = '#player';
Connector.artistSelector = '#artist';
Connector.trackSelector = '#title';
Connector.trackArtSelector = '#imgCover';
Connector.albumSelector = '#album';

Connector.isPlaying = () => Util.hasElementClass('#player', 'playing');

const filter = MetadataFilter.createFilter({
	artist: removeVonPrefix,
	album: removeAlbumPrefix,
});

Connector.applyFilter(filter);

function removeVonPrefix(text) {
	return text.replace('von ', '');
}

function removeAlbumPrefix(text) {
	return text.replace(', Album: ', '');
}


<|start_filename|>src/connectors/fip.js<|end_filename|>
'use strict';

const playerBar = '.RFplayer';

Connector.playerSelector = playerBar;

Connector.artistSelector = `${playerBar} .media-informations a:first-child`;

Connector.trackSelector = `${playerBar} .media-informations a:last-child`;

Connector.currentTimeSelector = `${playerBar} .start-time`;

Connector.durationSelector = `${playerBar} .end-time`;

Connector.trackArtSelector = `${playerBar} .cover img`;

Connector.pauseButtonSelector = `${playerBar} .ico-stop`;

// function removeYearFromArtist(text) {
// 	const regexp = new RegExp(/\s+\(\d{4}\)(?=[^\s+(\d{4})]*$)/gm);
// 	return text.replace(regexp, '');
// }


<|start_filename|>src/connectors/pinguinradio.js<|end_filename|>
'use strict';

const trackInfoSelector = '.stream-title';
const playButtonSelector = '.playbutton .material-icons';

Connector.playerSelector = '.audio-player';

Connector.getArtistTrack = () => {
	const trackInfoElement = document.querySelector(trackInfoSelector);
	if (!trackInfoElement) {
		return null;
	}

	const [trackElement, , artistElement] = trackInfoElement.childNodes;
	return {
		artist: artistElement && artistElement.textContent,
		track: trackElement && trackElement.textContent,
	};
};

Connector.isPlaying = () => {
	const playButton = document.querySelector(playButtonSelector);
	if (playButton) {
		return playButton.textContent.includes('pause');
	}

	return true;
};


<|start_filename|>src/connectors/turntable.fm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.room-view-board';

Connector.artistSelector = '.songboard-artist';

Connector.trackSelector = '.songboard-song';

Connector.currentTimeSelector = '.songboard-time';

Connector.remainingTimeSelector = '.songboard-time-left';

Connector.applyFilter(MetadataFilter.getYoutubeFilter());


<|start_filename|>src/connectors/thrilljockey.js<|end_filename|>
'use strict';

Connector.playerSelector = '.audio-player';

Connector.pauseButtonSelector = `${Connector.playerSelector} .audio-player--playing`;

Connector.trackSelector = `${Connector.playerSelector} .audio-player__title`;

Connector.getTrack = () => {
	const trackName = Util.getTextFromSelectors(Connector.trackSelector);
	return trackName.substr(trackName.indexOf(' ') + 1);
};

Connector.artistSelector = '.product-info-holder a[href^="/artists/"]';

Connector.albumSelector = '.product-info-holder .product-name';

Connector.trackArtSelector = '.product-image-holder .slick-slide[data-slick-index="0"] img';

Connector.getTrackArt = () => `${location.origin}${Util.extractImageUrlFromSelectors(Connector.trackArtSelector)}`;

Connector.currentTimeSelector = `${Connector.playerSelector} .audio-player__time-elapsed`;


<|start_filename|>src/connectors/openfm.js<|end_filename|>
'use strict';

Connector.playerSelector = '#openfm-player';

Connector.artistSelector = '#station-view .station-details > h3';

Connector.trackSelector = '#station-view .station-details > h2';

Connector.albumSelector = '#station-view .station-details > h4';

Connector.pauseButtonSelector = '.controls-con > .stop-btn';


<|start_filename|>src/connectors/sber-zvuk-dom-inject.js<|end_filename|>
'use strict';

function getTrackInfo(track, currentTime) {
	const { title, credits, duration, id, image, release_title: album } = track;
	const trackArt = image.src.replace('{size}', '400x400');

	return {
		track: title,
		artist: credits,
		uniqueID: id,
		duration,
		trackArt,
		currentTime,
		album,
	};
}

function queueEvent(event) {
	let type = null;
	let queue;

	if (event.type !== 'update') {
		return;
	}

	switch (event.name) {
		case 'state':
			type = event.newValue;
			queue = event.object.Queue;
			break;
		case 'currentTime':
		case 'current_idx':
			queue = event.name === 'current_idx' ? event.object : event.object.Queue;
			type = 'currentTime';
			break;
		default:
			return;
	}

	const trackInfo = getTrackInfo(queue.currentTrack, queue.Player.currentTime);

	window.postMessage({
		sender: 'web-scrobbler',
		type,
		trackInfo,
	}, '*');
}

function setupEventListeners() {
	window.newPlayer.$mobx.observe(queueEvent);
}

setupEventListeners();


<|start_filename|>src/connectors/jb.fm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.musica-info';

Connector.artistSelector = '#nome-artista';

Connector.trackSelector = '#nome-musica';

Connector.trackArtSelector = '#capa-album';

Connector.isPlaying = () => Util.hasElementClass('#botao-play', 'tocando');


<|start_filename|>src/connectors/soundcloud.js<|end_filename|>
'use strict';

const artistSelector = '.playbackSoundBadge__titleContextContainer > a';
const trackSelector = '.playbackSoundBadge__titleLink > span:nth-child(2)';
const trackArtSelector = '.playControls span.sc-artwork';
const durationSelector = '.playbackTimeline__duration > span:nth-child(2)';

Connector.playerSelector = '.playControls';

Connector.currentTimeSelector = '.playbackTimeline__timePassed > span:nth-child(2)';

Connector.getArtistTrack = () => {
	let { artist, track } = Util.processSoundCloudTrack(
		Util.getTextFromSelectors(trackSelector)
	);
	if (!artist) {
		artist = Util.getTextFromSelectors(artistSelector);
	}

	return { artist, track };
};

Connector.getTrackArt = () => {
	const trackArtUrl = Util.extractImageUrlFromSelectors(trackArtSelector);
	if (trackArtUrl) {
		return trackArtUrl.replace('-t50x50.', '-t200x200.');
	}

	return null;
};

Connector.getDuration = () => {
	const time = getDurationOrRemainingTime();
	return time > 0 ? time : null;
};

Connector.getRemainingTime = () => {
	const time = getDurationOrRemainingTime();
	return time < 0 ? time : null;
};

Connector.isPlaying = () => $('.playControl').hasClass('playing');

const filterArtistPremiereRules = [
	{ source: /^\s*Premiere.*:\s*/i, target: '' },
	{ source: /^\s*\*\*Premiere\*\*\s*/i, target: '' },
];

const filterTrackPremiereRules = [
	{ source: /\[.*Premiere.*\]/i, target: '' },
];

function filterArtistPremiere(text) {
	return MetadataFilter.filterWithFilterRules(text, filterArtistPremiereRules);
}

function filterTrackPremiere(text) {
	return MetadataFilter.filterWithFilterRules(text, filterTrackPremiereRules);
}

Connector.applyFilter(MetadataFilter.getYoutubeFilter().append({
	artist: filterArtistPremiere,
	track: filterTrackPremiere,
}));

function getDurationOrRemainingTime() {
	return Util.getSecondsFromSelectors(durationSelector);
}


<|start_filename|>src/connectors/pocketcasts.js<|end_filename|>
'use strict';

Connector.playerSelector = '.player-controls';

Connector.artistSelector = '.podcast-title';

Connector.trackSelector = '.episode-title';

Connector.playButtonSelector = '.play_button';

Connector.currentTimeSelector = '.current-time';

Connector.remainingTimeSelector = '.time-remaining';

Connector.trackArtSelector = '.player-image img';

Connector.isPodcast = () => true;


<|start_filename|>src/connectors/filmmusic.io.js<|end_filename|>
'use strict';

Connector.playerSelector = '.audioplayer-wrapper';
Connector.artistSelector = '.isplaying .fm-song-top-info a';
Connector.trackSelector = '.isplaying .fm-song-top-info h5';
Connector.trackArtSelector = '.isplaying .fm-song-left img';

Connector.isPlaying = () => Util.hasElementClass('.start', 'hidden');


<|start_filename|>src/connectors/brainfm.js<|end_filename|>
'use strict';

Connector.playerSelector = '[class^="Controls__wrapper___"]';

Connector.trackSelector = `${Connector.playerSelector} [class^="Controls__brainState___"]`;

Connector.pauseButtonSelector = `${Connector.playerSelector} svg[class^=PlayControl__pause___]`;

Connector.getArtist = () => 'Brain.fm';


<|start_filename|>src/core/background/storage/browser-storage.js<|end_filename|>
'use strict';

define((require) => {
	const browser = require('webextension-polyfill');
	const StorageWrapper = require('storage/wrapper');

	const LOCAL = 0;
	const SYNC = 1;

	/**
	 * This storage contains the connector options values.
	 * @see DEFAULT_CONNECTOR_OPTIONS_MAP object in `config` module.
	 */
	const CONNECTORS_OPTIONS = 'Connectors';

	/**
	 * This storage contains the connector override options values.
	 * @see DEFAULT_CONNECTOR_OVERRIDE_OPTIONS_MAP object in `config` module.
	 */
	const CONNECTORS_OVERRIDE_OPTIONS = 'ConnectorsOverrideOptions';

	/**
	 * This storage contains custom URL patterns defined by an user.
	 *
	 * The format of storage data is following:
	 * {
	 *     connector_id: [URL_pattern_1, URL_pattern_2, ...],
	 *     ...
	 * }
	 */
	const CUSTOM_PATTERNS = 'customPatterns';

	/**
	 * This storage contains data used to manage notifications.
	 *
	 * The `changelog` section contains data is used to check
	 * if user notified of changelog of certain version.
	 *
	 * The format of storage data is following:
	 * {
	 *     changelog: {
	 *     	   // `ver` is the extention version, e.g. 'v2.15.1'
	 *     	   // `true` means the notification of the version changelog
	 *     	   // was displayed.
	 *         ver: true,
	 *         ...
	 *     }
	 * }
	 */
	const NOTIFICATIONS = 'Notifications';

	/**
	 * This storage contains the song data saved by an user.
	 * The format of storage data is following:
	 * {
	 *     song_id: {
	 *         artist: 'Artist name',
	 *         track: 'Track name',
	 *         album: 'Album name', // Optional property
	 *     },
	 *     ...
	 * }
	 */
	const LOCAL_CACHE = 'LocalCache';

	/**
	 * This storage contains the options values.
	 * @see DEFAULT_OPTIONS_MAP object in `config` module.
	 */
	const OPTIONS = 'Options';

	/**
	 * This storage contains the data saved and used by the extension core.
	 * The format of storage data is following:
	 * {
	 *     appVersion: 'Extension version',
	 * }
	 */
	const CORE = 'Core';

	const storageTypeMap = {
		[CONNECTORS_OPTIONS]: SYNC,
		[CONNECTORS_OVERRIDE_OPTIONS]: SYNC,
		[CUSTOM_PATTERNS]: SYNC,
		[NOTIFICATIONS]: SYNC,
		[OPTIONS]: SYNC,

		[LOCAL_CACHE]: LOCAL,
		[CORE]: LOCAL,
	};

	/**
	 * Return storage by given namespace according storage type map.
	 * @param  {String} namespace Storage namespace
	 * @return {Object} StorageWrapper instance
	 * @throws {Error} if unknown namespace is specified
	 */
	function getStorage(namespace) {
		const storageType = storageTypeMap[namespace];
		switch (storageType) {
			case SYNC:
				return getSyncStorage(namespace);
			case LOCAL:
				return getLocalStorage(namespace);
			default:
				throw new Error(`Unknown storage namespace: ${namespace}`);
		}
	}

	/**
	 * Return storage by given scrobbler storage namespace.
	 * @param  {String} namespace Scrobbler storage namespace
	 * @return {Object} StorageWrapper instance
	 */
	function getScrobblerStorage(namespace) {
		return getLocalStorage(namespace);
	}

	/**
	 * Return local storage wrapped into StorageWrapper object.
	 * @param  {String} namespace Scrobbler storage namespace
	 * @return {Object} StorageWrapper instance
	 */
	function getLocalStorage(namespace) {
		const storageArea = browser.storage.local;
		return new StorageWrapper(storageArea, namespace);
	}

	/**
	 * Return sync storage wrapped into StorageWrapper object.
	 * Local storage is used as fallback.
	 * @param  {String} namespace Scrobbler storage namespace
	 * @return {Object} StorageWrapper instance
	 */
	function getSyncStorage(namespace) {
		const storageArea = browser.storage.sync || browser.storage.local;
		return new StorageWrapper(storageArea, namespace);
	}

	/**
	 * Object that helps to get wrapped storage.
	 */
	return {
		getStorage, getScrobblerStorage,

		getLocalStorage, getSyncStorage,

		CONNECTORS_OPTIONS, CONNECTORS_OVERRIDE_OPTIONS, CUSTOM_PATTERNS,
		NOTIFICATIONS, LOCAL_CACHE, OPTIONS, CORE,
	};
});


<|start_filename|>src/connectors/calm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.MiniSessionPlayer__PlayerBar-du0okt-4';
Connector.artistSelector = '.MiniSessionPlayer__Subtitle-du0okt-11';
Connector.trackSelector = '.MiniSessionPlayer__Title-du0okt-10';
Connector.trackArtSelector = '.MiniSessionPlayer__IconImage-du0okt-9';
Connector.currentTimeSelector = '.dBVUFV > .jVXvOE > .eDQGaR > .cwMKJQ';
Connector.durationSelector = '.dBVUFV > .jVXvOE > .eDQGaR > .eguLQa';


<|start_filename|>src/connectors/queup.js<|end_filename|>
'use strict';

Connector.playerSelector = '.infoContainer';

Connector.getArtistTrack = () => {
	const ytVideoTitle = Util.getTextFromSelectors('.currentSong');
	const { artist, track } = Util.processYtVideoTitle(ytVideoTitle);

	return { artist, track };
};

Connector.applyFilter(MetadataFilter.getYoutubeFilter());


<|start_filename|>src/connectors/kkbox-legacy.js<|end_filename|>
'use strict';

Connector.artistSelector = '.artist-name>a';

Connector.trackSelector = '.song-name>a';

Connector.playerSelector = '.right-column';

Connector.trackArtSelector = '.cover-area img';

Connector.pauseButtonSelector = '#pauseBtn';

Connector.getTimeInfo = () => document.querySelector('#kkboxPlayer audio');

const filter = MetadataFilter.createFilter({
	track: getLocalName,
	artist: getLocalName,
});

Connector.applyFilter(filter);

function getLocalName(name) {
	// Sanity check - if parenthesis is not entirely non-ascii, or outside parenthesis is not entirely ascii, don't apply filter
	const localName = name.replace(/[ -~].*?\((.*?)\)/, '$1');
	return localName.match(/[!-~]/) ? name : localName;
}


<|start_filename|>src/connectors/datpiff.js<|end_filename|>
'use strict';

Connector.playerSelector = '#embed';

Connector.artistSelector = '.track.active > .title > span:nth-child(2)';

Connector.trackSelector = '.track.active > .title > span:nth-child(1)';

Connector.isPlaying = () => $('#btnPlay > svg').hasClass('fa-pause');


<|start_filename|>src/connectors/detektorfm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.playerinfo';

Connector.artistSelector = '.playerinfo__artist';

Connector.trackSelector = '.playerinfo__title';

Connector.trackArtSelector = '.playerinfo__cover img';

Connector.isPlaying = () => Util.hasElementClass('.dfm-play', 'is-playing');


<|start_filename|>src/connectors/byte.fm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.player';

Connector.artistTrackSelector = '.player-current-title';

Connector.isPlaying = () => Util.getTextFromSelectors('.player-play-button') !== 'play';

Connector.onReady = Connector.onStateChanged;


<|start_filename|>src/connectors/playlist-randomizer.js<|end_filename|>
'use strict';

Connector.playerSelector = '.MuiList-root';
Connector.trackArtSelector = '.Mui-selected .MuiListItemAvatar-root .MuiAvatar-circle .MuiAvatar-img';

const trackSelector = '.Mui-selected .MuiListItemText-root .MuiListItemText-primary';

Connector.getArtistTrack = () => {
	let { artist, track } = Util.processYtVideoTitle(Util.getTextFromSelectors(trackSelector));

	// Set to some default information that we have (probably "Song Title Song Artist" with a space)
	// so that the user can edit the info in the extension
	if (!artist) {
		artist = Util.getTextFromSelectors(trackSelector);
	}
	if (!track) {
		track = Util.getTextFromSelectors(trackSelector);
	}

	return { artist, track };
};

Connector.applyFilter(MetadataFilter.getYoutubeFilter());


<|start_filename|>src/core/background/main.js<|end_filename|>
'use strict';

/**
 * Background script entry point.
 */
require(['extension', 'util/migrate'], (...modules) => {
	const [Extension, Migrate] = modules;

	Migrate.migrate().then(() => {
		new Extension().start();
	});
});


<|start_filename|>src/connectors/rockandpop.cl.js<|end_filename|>
'use strict';

Connector.playerSelector = '#player';

Connector.artistSelector = '#top_player_TopPlayer_spanInfo3';

Connector.trackSelector = '#top_player_TopPlayer_spanInfo2';

Connector.playButtonSelector = '.mm_player_btn_play .fa .fa-play';

Connector.trackArtSelector = '.mm_media-left img';

Connector.currentTimeSelector = '.mm_current-time';

Connector.durationSelector = '.mm_total-time';

Connector.isScrobblingAllowed = () => {
	const artist = Connector.getArtist();
	if (artist) {
		return !artist.toLowerCase().includes('24/7');
	}
	return false;
};


<|start_filename|>scripts/dep-data.js<|end_filename|>
'use strict';

/* eslint-disable no-template-curly-in-string */

const bootstrapDir = 'bootstrap/dist';
const fontawesomeDir = '@fortawesome/fontawesome-free';

const rootDir = '.';

const vendorDir = `${rootDir}/src/vendor`;
const packageJsonPath = './../package.json';

const dependenciesInfo = {
	'blueimp-md5': {
		name: 'MD5',
		urlTemplate: 'https://github.com/blueimp/JavaScript-MD5/archive/v${ver}.zip',
		paths: [{
			srcPaths: [
				'blueimp-md5/js/md5.min.js',
			],
		}],
	},
	'metadata-filter': {
		name: 'MetadataFilter',
		urlTemplate: 'https://github.com/web-scrobbler/metadata-filter/releases/download/v${ver}/metadata-filter-${ver}.tgz',
		paths: [{
			srcPaths: [
				'metadata-filter/dist/filter.js',
			],
		}],
	},
	'webextension-polyfill': {
		name: 'WebextPolyfill',
		urlTemplate: 'https://unpkg.com/browse/webextension-polyfill@${ver}/dist/',
		paths: [{
			srcPaths: [
				'webextension-polyfill/dist/browser-polyfill.min.js',
			],
		}],
	},
	jquery: {
		name: 'jQuery',
		urlTemplate: 'https://code.jquery.com/jquery-${ver}.min.js',
		paths: [{
			srcPaths: [
				'jquery/dist/jquery.min.js',
			],
		}],
	},
	requirejs: {
		name: 'RequireJS',
		urlTemplate: 'https://github.com/requirejs/requirejs/archive/${ver}.zip',
		paths: [{
			srcPaths: [
				'requirejs/require.js',
			],
		}],
	},

	'@fortawesome/fontawesome-free': {
		name: 'FontAwesome',
		urlTemplate: 'https://use.fontawesome.com/releases/v${ver}/fontawesome-free-${ver}-web.zip',
		paths: [{
			srcPaths: [
				`${fontawesomeDir}/css/solid.min.css`,
				`${fontawesomeDir}/css/brands.min.css`,
				`${fontawesomeDir}/css/fontawesome.min.css`,
			],
			destDir: `${vendorDir}/fontawesome/css`,
		}, {
			srcPaths: [
				`${fontawesomeDir}/webfonts/fa-solid-900.woff2`,
				`${fontawesomeDir}/webfonts/fa-brands-400.woff2`,
			],
			destDir: `${vendorDir}/fontawesome/webfonts`,
		}],
	},
	bootstrap: {
		name: 'Bootstrap',
		urlTemplate: 'https://github.com/twbs/bootstrap/releases/download/v${ver}/bootstrap-${ver}-dist.zip',
		paths: [{
			srcPaths: [
				`${bootstrapDir}/js/bootstrap.bundle.min.js`,
			],
			destDir: `${vendorDir}/bootstrap/js`,
		}, {
			srcPaths: [
				`${bootstrapDir}/css/bootstrap.min.css`,
			],
			destDir: `${vendorDir}/bootstrap/css`,
		}],
	},
};

function getDependencies(packageJson) {
	const dependenciesArr = [];
	const { dependencies } = packageJson;

	for (const id in dependencies) {
		const version = dependencies[id];

		dependenciesArr.push({ id, version });
	}

	return dependenciesArr;
}

module.exports = {
	dependenciesInfo, getDependencies, rootDir, vendorDir, packageJsonPath,
};


<|start_filename|>src/connectors/audacy.js<|end_filename|>
'use strict';

Connector.playerSelector = '#radioPlayer';
Connector.artistSelector = '#showDescription';
Connector.trackSelector = '#showName';
Connector.trackArtSelector = '#poster';
Connector.pauseButtonSelector = '#playButton.stop';


<|start_filename|>src/connectors/youtube-embed.js<|end_filename|>
'use strict';

/**
 * Generic connector for Youtube embed videos.
 */

/**
 * CSS selector of video element.
 * @type {String}
 */
const VIDEO_SELECTOR = '.html5-main-video';

function setupConnector() {
	const videoElement = $(VIDEO_SELECTOR);
	// Skip frames with no video element
	if (videoElement.length === 0) {
		return;
	}

	videoElement.on('timeupdate', Connector.onStateChanged);

	Connector.getArtistTrack = () => {
		const videoTitle = $('.ytp-title-link').text();
		return Util.processYtVideoTitle(videoTitle);
	};

	Connector.getCurrentTime = () => videoElement.prop('currentTime');

	Connector.getDuration = () => videoElement.prop('duration');

	Connector.isPlaying = () => {
		return $('.html5-video-player').hasClass('playing-mode');
	};

	Connector.getUniqueID = () => {
		const videoUrl = $('.ytp-title-link').attr('href');
		return Util.getYtVideoIdFromUrl(videoUrl);
	};

	Connector.applyFilter(MetadataFilter.getYoutubeFilter());
}

setupConnector();


<|start_filename|>src/connectors/deltaradio.de.js<|end_filename|>
'use strict';

Connector.playerSelector = '#audioPlayer';

Connector.artistSelector = '.trackInfos-artist';

Connector.trackSelector = '.trackInfos-title';

Connector.pauseButtonSelector = '#stopButton';


<|start_filename|>src/connectors/shuffleone.js<|end_filename|>
'use strict';

Connector.playerSelector = '#root';

Connector.artistSelector = '.actions .artists';

Connector.trackSelector = '.actions .title';

Connector.trackArtSelector = '.cover > svg';

Connector.isPlaying = () => {
	const playingPath = 'M8 19c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2v10c0 1.1.9 2 2 2zm6-12v10c0 1.1.9 2 2 2s2-.9 2-2V7c0-1.1-.9-2-2-2s-2 .9-2 2z';
	return $('.bottom button:nth-child(3) > svg > path').attr('d') === playingPath;
};


<|start_filename|>src/connectors/weibo.js<|end_filename|>
'use strict';

Connector.playerSelector = '.PCD_mplayer';

Connector.trackSelector = '.name_bar';

Connector.artistSelector = '.author_bar';

Connector.pauseButtonSelector = '.ico_pause';

Connector.remainingTimeSelector = '.time_count';

Connector.trackArtSelector = '.music_pic img';

Connector.getUniqueID = () => {
	const text = Util.getAttrFromSelectors('.current', 'objectid');
	return text && text.split('_').pop();
};


<|start_filename|>src/connectors/deezer-dom-inject.js<|end_filename|>
'use strict';

initConnector();

function initConnector() {
	let retries = 0;
	if (window.dzPlayer && window.Events) {
		addEventListeners();
	} else if (retries < 6) {
		window.setTimeout(function() {
			initConnector();
			retries++;
		}, 1007);
	} else {
		console.warn('Web Scrobbler: Failed to initialize deezer connector!');
	}
}

function addEventListeners() {
	const ev = window.Events;
	ev.subscribe(ev.player.play, sendEvent);
	ev.subscribe(ev.player.paused, sendEvent);
	ev.subscribe(ev.player.resume, sendEvent);
}

function sendEvent() {
	window.postMessage({
		sender: 'web-scrobbler',
		type: 'DEEZER_STATE',
		trackInfo: getCurrentMediaInfo(),
		isPlaying: isPlaying(),
		isPodcast: isPodcast(),
	}, '*');
}

function getCurrentMediaInfo() {
	const player = window.dzPlayer;
	const currentMedia = player.getCurrentSong();

	// Radio stations don't provide track info
	if (currentMedia.EXTERNAL) {
		return null;
	}

	const mediaType = currentMedia.__TYPE__;
	const currentTime = player.getPosition();
	const duration = player.getDuration();

	let trackInfo = null;

	switch (mediaType) {
		case 'episode': {
			trackInfo = getEpisodeInfo(currentMedia);
			break;
		}

		case 'song': {
			trackInfo = getTrackInfo(currentMedia);
			break;
		}
	}

	if (!trackInfo) {
		console.warn(`Web Scrobbler: Unable to load track info for ${mediaType} media type`);
		return null;
	}

	trackInfo.currentTime = currentTime;
	trackInfo.duration = duration;

	return trackInfo;
}

function getTrackInfo(currentMedia) {
	let trackTitle = currentMedia.SNG_TITLE;
	const trackVersion = currentMedia.VERSION;
	if (trackVersion) {
		trackTitle = `${trackTitle} ${trackVersion}`;
	}

	return {
		artist: currentMedia.ART_NAME,
		track: trackTitle,
		album: currentMedia.ALB_TITLE,
		uniqueID: currentMedia.SNG_ID,
		trackArt: getTrackArt(currentMedia.ALB_PICTURE),
	};
}

function getEpisodeInfo(currentMedia) {
	return {
		artist: currentMedia.SHOW_NAME,
		track: currentMedia.EPISODE_TITLE,
		uniqueID: currentMedia.EPISODE_ID,
	};
}

function isPlaying() {
	return window.dzPlayer.isPlaying();
}

function isPodcast() {
	const currentMedia = window.dzPlayer.getCurrentSong();
	return currentMedia.__TYPE__ === 'episode';
}

function getTrackArt(pic) {
	return `https://e-cdns-images.dzcdn.net/images/cover/${pic}/264x264-000000-80-0-0.jpg`;
}


<|start_filename|>src/ui/options/export.js<|end_filename|>
'use strict';

define((require) => {
	const BrowserStorage = require('storage/browser-storage');

	const EXPORT_FILENAME = 'local-cache.json';

	const localCache = BrowserStorage.getStorage(BrowserStorage.LOCAL_CACHE);

	async function initialize() {
		$('#export-edited').click((e) => {
			e.preventDefault();
			exportLocalCache();
		});

		$('#import-edited').click((e) => {
			e.preventDefault();
			importLocalStorage();
		});
	}

	/**
	 * Export content of LocalCache storage to a file.
	 */
	async function exportLocalCache() {
		const data = await localCache.get();
		const dataStr = JSON.stringify(data, null, 2);
		const blob = new Blob([dataStr], { 'type': 'application/octet-stream' });
		const url = URL.createObjectURL(blob);

		const a = document.createElement('a');
		a.href = url;
		a.download = EXPORT_FILENAME;
		a.dispatchEvent(new MouseEvent('click'));
		a.remove();

		URL.revokeObjectURL(url);
	}

	/**
	 * Import LocalCache storage from a file.
	 */
	function importLocalStorage() {
		const fileInput = document.createElement('input');

		fileInput.style.display = 'none';
		fileInput.type = 'file';
		fileInput.accept = '.json';
		fileInput.acceptCharset = 'utf-8';

		document.body.appendChild(fileInput);
		fileInput.initialValue = fileInput.value;
		fileInput.onchange = readFile;
		fileInput.click();

		function readFile() {
			if (fileInput.value !== fileInput.initialValue) {
				const file = fileInput.files[0];

				const reader = new FileReader();
				reader.onloadend = (event) => {
					const dataStr = event.target.result;
					const data = JSON.parse(dataStr);

					localCache.update(data).then(fileInput.remove);

				};
				reader.readAsText(file, 'utf-8');
			}
		}
	}

	return { initialize };
});


<|start_filename|>src/connectors/streetvoice.js<|end_filename|>
'use strict';

function setupConnector() {
	if (isMobilePlayer()) {
		setupMobilePlayer();
	} else {
		setupDesktopPlayer();
	}
	setupCommonProperties();
}

function isMobilePlayer() {
	return Util.queryElements('#react-mobile-search').length > 0;
}

function setupDesktopPlayer() {
	Connector.playerSelector = '.now-playing';

	Connector.trackArtSelector = '.now-playing img';

	Connector.trackSelector = '.now-playing h4';

	Connector.artistSelector = '.now-playing h5';

	Connector.pauseButtonSelector = '.pause';
}

function setupMobilePlayer() {
	if (isListPlayer()) {
		setupListPlayer();
	} else {
		setupSinglePlayer();
	}

	Connector.pauseButtonSelector = '.playing button';
}

function isListPlayer() {
	return Util.queryElements('#item_box_list').length > 0;
}

function setupSinglePlayer() {
	Connector.playerSelector = '#react-player-wrapper';

	Connector.trackArtSelector = '.cover-block img';

	Connector.trackSelector = 'h1';

	Connector.artistSelector = '.user-info h4';

	Connector.albumSelector = '.cover-block~h5:nth-child(4)';
}

function setupListPlayer() {
	Connector.playerSelector = '.play,.playing';

	Connector.trackSelector = '.play,.playing .work-item-info h4';

	Connector.artistSelector = '.play,.playing h5';
}

function setupCommonProperties() {
	Connector.currentTimeSelector = `${Connector.playerSelector} .text-left`;

	Connector.durationSelector = `${Connector.playerSelector} .text-right`;

	Connector.getUniqueID = () => {
		return Util.getAttrFromSelectors(`${Connector.playerSelector
		} button[data-id]`, 'data-id');
	};
}
setupConnector();


<|start_filename|>src/connectors/epidemicsound.js<|end_filename|>
'use strict';

Connector.playerSelector = '[class*=PlayerUI__content]';

Connector.artistSelector = '[class*=CreativesLabel__container]';

Connector.trackSelector = '[class*=TrackInfo__title]';

Connector.timeInfoSelector = '[class*=TimeProgress__container]';

Connector.playButtonSelector = '[class*=PlaybackControls__playIcon]';


<|start_filename|>src/connectors/bbc-sounds.js<|end_filename|>
'use strict';

const trackItemSelector = '.sc-c-basic-tile';

setupConnector();

function setupConnector() {
	if (isLiveRadio()) {
		setupPropertiesForLiveRadio();
	} else {
		setupPropertierForOfflineRecord();
	}
}

function isLiveRadio() {
	return document.querySelector(trackItemSelector) === null;
}

// Example: any of live radios from https://www.bbc.co.uk/sounds
function setupPropertiesForLiveRadio() {
	Connector.playerSelector = '.radio-main';

	Connector.artistSelector = '.sc-c-track__artist';

	Connector.trackSelector = '.sc-c-track__title';
}

// Example: any of music mixes from https://www.bbc.co.uk/sounds
function setupPropertierForOfflineRecord() {
	const equalizerIconSelector = '.sc-c-equalizer';

	const artistSelector = '.sc-c-basic-tile__artist';
	const trackSelector = '.sc-c-basic-tile__title';

	Connector.playerSelector = '.sc-o-scrollable';

	Connector.getArtistTrack = () => {
		const artistTrackElement = document.querySelector(
			equalizerIconSelector
		);
		if (!artistTrackElement) {
			return null;
		}

		const trackItem = artistTrackElement.closest(trackItemSelector);
		const artist = trackItem.querySelector(artistSelector).textContent;
		const track = trackItem.querySelector(trackSelector).textContent;

		return { artist, track };
	};
}


<|start_filename|>src/connectors/radiotunes.js<|end_filename|>
'use strict';

const playerBar = '#webplayer-region';
const trackArtSelectors = [
	// Radiotunes
	'#art',
	// DI
	'.track-region .artwork img',
];

const filter = MetadataFilter.createFilter({ artist: removeTrailingDash });

Connector.playerSelector = playerBar;

Connector.artistSelector = '.artist-name';

Connector.trackSelector = '.track-name';

Connector.playButtonSelector = '.icon-play';

Connector.currentTimeSelector = '.timeinfo .time';

Connector.durationSelector = '.timeinfo .total';

Connector.getTrackArt = () => {
	const trackArtUrl = Util.extractImageUrlFromSelectors(trackArtSelectors);
	if (trackArtUrl) {
		// Remove 'size' param
		return trackArtUrl.split('?')[0];
	}
	return null;
};

Connector.isPlaying = () => {
	return $('#webplayer-region').attr('data-state') === 'playing';
};

Connector.applyFilter(filter);

function removeTrailingDash(text) {
	return text.replace(/\s-\s$/, '');
}


<|start_filename|>src/connectors/librefm.js<|end_filename|>
'use strict';

Connector.playerSelector = '#player';

Connector.artistSelector = '#artistname';

Connector.trackSelector = '#trackname';

Connector.playButtonSelector = '#play';

Connector.currentTimeSelector = '#currenttime';

Connector.durationSelector = '#duration';


<|start_filename|>src/connectors/pretzel.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({
	track: MetadataFilter.fixTrackSuffix,
	album: MetadataFilter.fixTrackSuffix,
});

Connector.applyFilter(filter);

const artistSelector = '.kzpiRD p:nth-child(2) a';

Connector.playerSelector = '.hOOKvw';

Connector.trackSelector = '.kzpiRD .oKpSL';

Connector.albumSelector = '.kzpiRD p:nth-child(3) a';

Connector.playButtonSelector = '.pretzel-icon-player_play';

Connector.trackArtSelector = '.rwQJb img';

Connector.currentTimeSelector = '.hcriLb p:nth-child(1)';

Connector.durationSelector = '.hcriLb p:nth-child(3)';

Connector.getArtist = () => getArtistsFromElement(artistSelector);

function getArtistsFromElement(selector) {
	const artistElements = document.querySelectorAll(selector);
	return Util.joinArtists(Array.from(artistElements));
}


<|start_filename|>src/connectors/nightride.fm.js<|end_filename|>
'use strict';

// A more specific selector simply wouldn't work
Connector.playerSelector = 'body';

Connector.artistTrackSelector = '#nowplaying';

Connector.playButtonSelector = '#playerPlay';


<|start_filename|>src/core/content/starter.js<|end_filename|>
'use strict';

/**
 * This script is injected to the page after the {@link BaseConnector} and
 * a custom connector are already in place and set up.
 *
 * As custom connectors should be declarative only without any actions triggered
 * on pageload, this starter is needed for connectors to start running
 */
(() => {
	if (window.STARTER_LOADED !== undefined) {
		Util.debugLog('Starter is already loaded', 'warn');
		return;
	}
	window.STARTER_LOADED = true;

	if (isConnectorInvalid()) {
		Util.debugLog('You have overwritten or unset the Connector object', 'warn');
		return;
	}

	setupStateListening();

	/* Internal functions. */

	function isConnectorInvalid() {
		return typeof Connector === 'undefined' || !(Connector instanceof BaseConnector);
	}

	function setupStateListening() {
		new Reactor(Connector);

		if (Connector.playerSelector === null) {
			Util.debugLog('`Connector.playerSelector` is empty. The current connector is expected to manually detect state changes', 'info');
			return;
		}

		Util.debugLog('Setting up observer');

		const observeTarget = retrieveObserveTarget();
		if (observeTarget !== null) {
			setupObserver(observeTarget);
		} else {
			Util.debugLog(`Element '${Connector.playerSelector}' is missing`, 'warn');
			setupSecondObserver();
		}
	}

	function setupObserver(observeTarget) {
		const observer = new MutationObserver(Connector.onStateChanged);
		const observerConfig = {
			childList: true, subtree: true,
			attributes: true, characterData: true,
		};

		observer.observe(observeTarget, observerConfig);

		Util.debugLog(`Used '${Connector.playerSelector}' to watch changes.`);
	}

	function setupSecondObserver() {
		const playerObserver = new MutationObserver(() => {
			const observeTarget = retrieveObserveTarget();
			if (observeTarget !== null) {
				playerObserver.disconnect();
				setupObserver(observeTarget);
			}
		});

		const playerObserverConfig = {
			childList: true, subtree: true,
			attributes: false, characterData: false,
		};
		playerObserver.observe(document, playerObserverConfig);
	}

	function retrieveObserveTarget() {
		return document.querySelector(Connector.playerSelector);
	}
})();


<|start_filename|>src/connectors/monstercat.js<|end_filename|>
'use strict';

const mainPlayerBar = '.controls';

if (isMainPlayer()) {
	Util.debugLog('Setup the connector for main player');

	setupMainPlayer();
} else {
	Util.debugLog('Setup the connector for Gold player');

	setupGoldPlayer();
}

function isMainPlayer() {
	return document.querySelector(mainPlayerBar) !== null;
}

function setupMainPlayer() {
	Connector.playerSelector = mainPlayerBar;

	Connector.artistTrackSelector = `${mainPlayerBar} .scroll-title`;

	Connector.pauseButtonSelector = `${mainPlayerBar} .fa-pause`;

	Connector.useMediaSessionApi();
}

function setupGoldPlayer() {
	Connector.playerSelector = '.control-bar';

	Connector.trackSelector = '.song-info .song-title';

	Connector.getArtist = () => {
		const artists = Util.queryElements('.song-info .artists-list a');

		if (artists !== null) {
			return Util.joinArtists(artists.toArray());
		}

		return null;
	};

	Connector.trackArtSelector = '.album-art';

	Connector.timeInfoSelector = '.active-song-time';

	Connector.pauseButtonSelector = '.play-pause .fa-pause';
}


<|start_filename|>src/connectors/dashradio.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({
	artist: removeByText,
});

Connector.playerSelector = '#music-dataview-container';

Connector.artistSelector = '#artist-text';

Connector.trackSelector = '#title-text';

Connector.applyFilter(filter);

function removeByText(text) {
	return text.replace('by ', '');
}


<|start_filename|>src/connectors/jamstash.js<|end_filename|>
'use strict';

Connector.playerSelector = '#player';

Connector.artistSelector = '#songdetails .artist';

Connector.trackSelector = '#songdetails .song';

Connector.albumSelector = '#songdetails .album';

Connector.playButtonSelector = '#player .PlayTrack';

Connector.getTrackArt = () => {
	const trackArtUrl = Util.extractImageUrlFromSelectors('#coverart img');
	// Remove size flag from track art URL
	return trackArtUrl && trackArtUrl.replace(/&size=\d+/, '');
};

Connector.currentTimeSelector = '#played';

Connector.durationSelector = '#duration';

Connector.isScrobblingAllowed = () => {
	return Connector.getDuration() !== 0;
};


<|start_filename|>src/connectors/radioco.js<|end_filename|>
'use strict';

Connector.playerSelector = '.radioco-player';

Connector.trackSelector = '.track-name';

Connector.artistSelector = '.track-artist';

Connector.trackArtSelector = '.current-artwork';

Connector.isPlaying = () => {
	return $('.radioco-player').hasClass('playing');
};


<|start_filename|>src/connectors/cbcmusic.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({
	track: cleanupTrack,
	artist: cleanupArtist,
});

Connector.playerSelector = 'div.playerUI';

Connector.artistSelector = 'div.playerUI div.marqueeContent div';

Connector.trackSelector = 'div.playerUI div.marqueeContent div';

Connector.currentTimeSelector = 'div.scrubControlCurrentTime';

Connector.durationSelector = 'div.scrubControlDuration';
Connector.isPlaying = () => {
	return Util.hasElementClass('button.playButton', 'playButton--pause');
};

Connector.onReady = Connector.onStateChanged;

Connector.applyFilter(filter);

function cleanupTrack(track) {
	// Extract a track title from a `Track by Artist` string.
	return track.replace(/^(.*?)\s(by)\s(.*?)$/i, '$1');
}

function cleanupArtist(artist) {
	// Extract the artist from a `Track by Artist` string.
	return artist.replace(/^(.*?)\s(by)\s(.*?)$/i, '$3');
}


<|start_filename|>src/connectors/keakie.js<|end_filename|>
'use strict';

Connector.playerSelector = '.player-full-playback-controls';

Connector.playButtonSelector = '.play.button.--is-invisible';

Connector.getArtistTrack = () => {
	const artistTrack = Util.getTextFromSelectors('.current-track-and-artist');

	return Util.splitArtistTrack(artistTrack, ['-'], { swap: true });
};


<|start_filename|>src/connectors/ibroadcast.js<|end_filename|>
'use strict';

Connector.playerSelector = '.mgr-player';

Connector.trackSelector = '.mgr-player-title';

Connector.artistSelector = '.mgr-player-artist';

Connector.albumSelector = '.mgr-player-album';

Connector.currentTimeSelector = '.mgr-player-current-time';

Connector.durationSelector = '.mgr-player-duration';

Connector.trackArtSelector = '.mgr-player-artwork-image';

Connector.isPlaying = () => Util.hasElementClass(
	['.mgr-player-play'],
	'icon-player-pause'
);


<|start_filename|>src/connectors/hypem-albums.js<|end_filename|>
'use strict';

Connector.playerSelector = '.hype-player';

Connector.artistSelector = '#album-header-artist';

Connector.albumSelector = '#album-header-title';

Connector.trackSelector = 'li.active .title';

Connector.trackArtSelector = 'img#album-big';

Connector.isPlaying = () => $('.hype-player').hasClass('playing');

Connector.currentTimeSelector = '.current_pos';

Connector.durationSelector = 'li.active .duration';


<|start_filename|>src/connectors/genie.js<|end_filename|>
'use strict';

Connector.playerSelector = '#music-player';

Connector.artistSelector = '#ArtistNameArea';

Connector.trackSelector = '#SongTitleArea';

Connector.getAlbum = () => {
	return $('#AlbumImgArea > img').attr('alt');
};

Connector.currentTimeSelector = 'span.fp-elapsed';

Connector.durationSelector = 'span.fp-remaining';

Connector.isPlaying = () => {
	const btn = $('.fp-playbtn').first();
	return btn.html() === '일시정지'; // if text '일시정지' in button, it means 'pause' because music is playing.
};

Connector.trackArtSelector = '#AlbumImgArea img';


<|start_filename|>src/connectors/nicovideo.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({
	track: removeDecorationText,
	artist: selectPrimaryName,
});

const videoSelector = '.MainVideoPlayer > video';

Connector.playerSelector = '.PlayerContainer';

Connector.artistSelector = '.VideoOwnerInfo-pageLink';

Connector.trackSelector = '.VideoTitle';

Connector.durationSelector = '.PlayerPlayTime-duration';

Connector.currentTimeSelector = '.PlayerPlayTime-playtime';

Connector.isPlaying = () => {
	const video = document.querySelector(videoSelector);
	if (!video) {
		return false;
	}

	return video.currentTime > 0 && !video.paused && !video.ended;
};

Connector.getUniqueID = () => window.location.pathname.replace(/\/$/, '').replace(/.*\//, '');

Connector.applyFilter(filter);

function removeDecorationText(text) {
	// Usual track name on Niconico is something like "【Hatsune Miku】Track Name【Original】"
	const decorationPattern = /^【[^】]*】\s*|\s*【[^【]*】$|^\[[^\]]*\]\s*|\s*\[[^[]*\]$/g;
	// Or, something like "Track Name / Composer, Singer, etc."
	const extraInfoPattern = /\s*(\/[^/]+|／[^／]+|[^\da-zA-Z]-[^\da-zA-Z][^-]+|\sfeat\..+|\sft\..+)$/i;
	// Or, something like "Original Song『Track Name』MV"
	const titlePattern = /「(.+)」|『(.+)』|'(.+)'|"(.+)"/;

	const match = text.match(titlePattern);
	if (match) {
		return match[1] || match[2] || match[3] || match[4];
	}

	return text.replace(decorationPattern, '').replace(extraInfoPattern, '');
}

function selectPrimaryName(text) {
	// Niconico shows the artist name like "Artist さん" when you're a Japanese user
	const nameWithoutSuffix = text.replace(/\sさん$/, '');

	// Some artists show their alternative name as "Artist Name / Alternative Artist Name"
	// or "Artist Name(Alternative Artist Name)"
	const namePattern = /^(.+?)\s*([/／].+|\([^(]+\)$|（[^（]+）$)/;
	const match = nameWithoutSuffix.match(namePattern);
	if (match) {
		return match[1];
	}

	return nameWithoutSuffix;
}


<|start_filename|>src/connectors/joox.js<|end_filename|>
'use strict';

const playerSelector = '.kIpMXu';
const artistSelector = `${playerSelector} .kOwKMw a`;

Connector.useMediaSessionApi();

Connector.playerSelector = playerSelector;

Connector.trackSelector = '.xXMwE';

Connector.trackArtSelector = `${playerSelector} .XVHIE img`;

Connector.getArtist = () => {
	const artistNodes = document.querySelectorAll(artistSelector);
	return Util.joinArtists(Array.from(artistNodes));
};

Connector.currentTimeSelector = '#currentTime';

Connector.durationSelector = '.GeFxq';

Connector.playButtonSelector = '.playerIcon--play';


<|start_filename|>src/connectors/eulerbeats.js<|end_filename|>
'use strict';

Connector.playerSelector = '#root';

Connector.isPlaying = () =>
	!!document.querySelector('.eulerbeats-main.eulerbeats-is-playing');

Connector.getUniqueID = () => getSeed();

Connector.getAlbum = () => 'Euler';

Connector.getArtist = () => 'EulerBeats';

Connector.getTrack = () => document.title.split(' | ')[1];

Connector.getTrackArt = () => {
	const release = getRelease();
	return `https://media.eulerbeats.com/${
		release === 'genesis' ? '' : `${release}/`
	}${getSeed()}.png`;
};

function getRelease() {
	return window.location.pathname.split('/')[1];
}

function getSeed() {
	const release = getRelease();
	let tokenId = Number(window.location.pathname.split('/')[2]);
	if (release === 'enigma') {
		if (tokenId % 2 !== 0) {
			tokenId -= 1;
		}
		return tokenId.toString(16).padStart(12, '0');
	} else if (release === 'genesis') {
		if (tokenId > 0x4000000000) {
			tokenId = tokenId - 0x4000000000;
		}
		return tokenId.toString(16).padStart(10, '0');
	}
}


<|start_filename|>tests/background/custom-patterns.js<|end_filename|>
'use strict';

const expect = require('chai').expect;
const CustomPatterns = require('../../src/core/background/storage/custom-patterns');

/**
 * Run all tests.
 */
function runTests() {
	it('should return no patterns', (done) => {
		CustomPatterns.getAllPatterns().then((data) => {
			expect({}).to.be.deep.equal(data);
			done();
		});
	});

	it('should set patterns for connector', (done) => {
		const patterns = ['1', '2'];
		const expectedData = {
			connector: patterns,
		};
		CustomPatterns.setPatterns('connector', patterns).then(() => {
			CustomPatterns.getAllPatterns().then((data) => {
				expect(expectedData).to.be.deep.equal(data);
				done();
			});
		});
	});

	it('should clear custom patterns', (done) => {
		CustomPatterns.resetPatterns('connector').then(() => {
			CustomPatterns.getAllPatterns().then((data) => {
				expect({}).to.be.deep.equal(data);
				done();
			});
		});
	});
}

runTests();


<|start_filename|>src/connectors/relaxfm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.main-player__wrapp';

Connector.artistSelector = '.TitleExecutorAir';

Connector.trackSelector = '.TitleTracksAir';

Connector.pauseButtonSelector = '.toggleplayer.stop';


<|start_filename|>src/connectors/9sky.js<|end_filename|>
'use strict';

// http://www.fileformat.info/info/unicode/category/Ps/list.htm
const rightBrackets = ')|༻༽᚜‛‟⁆⁾₎⌉⌋〉❩❫❭❯❱❳❵⟆⟧⟩⟫⟭⟯⦄⦆⦈⦊⦌⦎⦐⦒⦔⦖⦘⧙⧛⧽⸣⸥⸧⸩⹃〉》」』】〕〗〙〛〞﵀︘︶︸︺︼︾﹀﹂﹄﹈﹚﹜﹞）＼｜｠｣';
const titleBrackets = '〉》」』】〕〗〙〛〞﵀︘︶︸︺︼︾﹀﹂﹄﹈';

$('audio, video').bind('playing pause timeupdate', Connector.onStateChanged);

Connector.getDuration = () => $('audio, video').prop('duration');

Connector.getCurrentTime = () => $('audio, video').prop('currentTime');

Connector.isPlaying = () => {
	const media = $('audio, video').get(0);
	return media.currentTime && !media.paused && !media.ended;
};

Connector.playerSelector = '.music_bg, .mv_box';

Connector.trackSelector = '.musicright_box2, #name_h3 a:first-child';

Connector.artistSelector = '.musicright_box3 a, #name_h3 a:last-child';

Connector.trackArtSelector = '#playimg';

Connector.getUniqueID = () => {
	const text = $('.li1 img').parents('ul').attr('id');
	const match = /id=(\d+)/g.exec(location.search);
	return text && `a${text.slice(4)}` ||
		match && `v${match[1]}` ||
		null;
};

const filter = MetadataFilter.createFilter({ track: filterTrack });

function filterTrack(text) {
	const regex = new RegExp(`^.*([${rightBrackets}]).*$`);

	const filteredText = text.replace(regex, filterBrackets);
	return filteredText.replace(/[\s_—－-][^_—－-]+(?:版|version|mv)\s*$/i, '');
}

function filterBrackets(text, bracket) {
	const i = text.lastIndexOf(bracket);
	const j = text.indexOf(String.fromCharCode(bracket.charCodeAt() - 1));
	const b = text.slice(j + 1, i);
	if (!/(?:版|version|mv)\s*$/i.test(b)) {
		return titleBrackets.includes(bracket) ? b : text;
	}

	const filteredText = Array.from(text);
	filteredText.splice(j, i - j + 1);

	return filteredText.join('');
}

Connector.applyFilter(filter);


<|start_filename|>src/core/i18n.js<|end_filename|>
'use strict';

/**
 * Node attributes that define how node content will be localized.
 *
 * There're following supported attributes:
 *  - i18n: replace value of `textContent` property by localized text;
 *  - i18n-title: replace value of `title` attribute by localized text;
 *  - i18n-placeholder: replace value of `placeholder` attribute by localized text.
 *
 * @type {Array}
 */
const I18N_ATTRS = ['i18n', 'i18n-title', 'i18n-placeholder'];

const domParser = new DOMParser();

/**
 * Localize all nodes.
 */
function localizeDocument() {
	// Localize static nodes
	localizeElementChilds(document);

	// Localize dynamic nodes
	new MutationObserver((mutations) => {
		for (const mutation of mutations) {
			for (const node of mutation.addedNodes) {
				localizeElement(node);
				localizeElementChilds(node);
			}
		}
	}).observe(document.body, {
		childList: true,
		subtree: true,
	});
}

/**
 * Localize given element.
 * @param  {Object} element Element to localize
 */
function localizeElement(element) {
	if (!(element instanceof Element)) {
		return;
	}

	for (const attr of I18N_ATTRS) {
		if (!element.hasAttribute(attr)) {
			continue;
		}

		const tag = element.getAttribute(attr);
		const text = chrome.i18n.getMessage(tag) || tag;

		switch (attr) {
			case 'i18n':
				if (hasHtmlTags(text)) {
					const nodes = makeNodes(text);
					if (nodes) {
						nodes.forEach((n) => {
							element.appendChild(n);
						});
					} else {
						// Fallback
						element.textContent = text;
					}
				} else {
					element.textContent = text;
				}
				break;

			case 'i18n-title':
				element.setAttribute('title', text);
				break;

			case 'i18n-placeholder':
				element.setAttribute('placeholder', text);
				break;
		}
	}
}

/**
 * Localize children of given element.
 * @param  {Object} element Element to localize
 */
function localizeElementChilds(element) {
	switch (element.nodeType) {
		case Node.ELEMENT_NODE:
		case Node.DOCUMENT_NODE:
			for (const attr of I18N_ATTRS) {
				const nodes = element.querySelectorAll(`[${attr}]`);
				nodes.forEach(localizeElement);
			}
			break;
	}
}

/**
 * Create array of nodes which can be applied to node to be translated.
 * @param  {String} rawHtml String contains HTML code
 * @return {Array} Array of nodes from given text
 */
function makeNodes(rawHtml) {
	const body = domParser.parseFromString(rawHtml, 'text/html').body;
	return [...body.childNodes].filter((a) => {
		return a.nodeType === a.TEXT_NODE || a.tagName === 'A';
	});
}

/**
 * Check if given text contains HTML tags
 * @param  {String} text String supposed to have HTML tags
 * @return {Boolean} Check result
 */
function hasHtmlTags(text) {
	return /<.+?>/.test(text);
}

document.addEventListener('DOMContentLoaded', localizeDocument);


<|start_filename|>src/connectors/youtube-music.js<|end_filename|>
'use strict';

/**
 * Quick links to debug and test the connector:
 *
 * https://music.youtube.com/playlist?list=OLAK5uy_kDEvxPASaVnoSjOZViKEn4S3iVaueN0UI
 * Multiple artists
 *
 * https://music.youtube.com/playlist?list=OLAK5uy_k-OR_rCdS5UNV22eIhAOWLMZbbxa20muQ
 * Auto-generated YouTube video (and generic track on YouTube Music)
 *
 * https://music.youtube.com/watch?v=Ap1fDjCXQrU
 * Regular YouTube video which contains artist and track names in video title
 *
 * https://music.youtube.com/watch?v=hHrvuQ4DwJ8
 * Regular YouTube video which contains track name in video title and
 * artist name as a channel name
 *
 * https://music.youtube.com/library/uploaded_songs
 * Uploaded songs have different artist and track selectors
 */

const trackArtSelector = '.ytmusic-player-bar.image';

const artistSelectors = [
	// Base selector, combining both new and old
	'.ytmusic-player-bar.byline [href*="channel/"]:not([href*="channel/MPREb_"]):not([href*="browse/MPREb_"])',

	// Old selector for self-uploaded music
	'.ytmusic-player-bar.byline [href*="feed/music_library_privately_owned_artist_detaila_"]',

	// New selector for self-uploaded music
	'.ytmusic-player-bar.byline [href*="browse/FEmusic_library_privately_owned_artist_detaila_"]',
];
const albumSelectors = [
	// Old base selector, leaving in case removing it would break something
	'.ytmusic-player-bar [href*="channel/MPREb_"]',

	// New base selector
	'.ytmusic-player-bar [href*="browse/MPREb_"]',

	// Old selector for self-uploaded music, also leaving for now
	'.ytmusic-player-bar [href*="feed/music_library_privately_owned_release_detailb_"]',

	// New selector for self-uploaded music
	'.ytmusic-player-bar [href*="browse/FEmusic_library_privately_owned_release_detailb_"]',
];

const trackSelector = '.ytmusic-player-bar.title';
const adSelector = '.ytmusic-player-bar.advertisement';

const playButtonSelector =
	'.ytmusic-player-bar.play-pause-button #icon > svg > g > path';
const playingPath = 'M6 19h4V5H6v14zm8-14v14h4V5h-4z';

Connector.playerSelector = 'ytmusic-player-bar';

Connector.getTrackArt = () => {
	const trackArtUrl = Util.extractImageUrlFromSelectors(trackArtSelector);
	if (trackArtUrl) {
		return trackArtUrl.substring(0, trackArtUrl.lastIndexOf('='));
	}
	return null;
};

Connector.isTrackArtDefault = (url) => {
	// Self-uploaded tracks could not have cover arts
	return url.includes('cover_track_default');
};

Connector.albumSelector = albumSelectors;

function hasVideoAlbum() {
	return !!Connector.getAlbum();
}

Connector.getArtistTrack = () => {
	let artist;
	let track;

	if (hasVideoAlbum()) {
		artist = getArtists();
		track = Util.getTextFromSelectors(trackSelector);
	} else {
		({ artist, track } = Util.processYtVideoTitle(
			Util.getTextFromSelectors(trackSelector)
		));
		if (!artist) {
			artist = getArtists();
		}
	}
	return { artist, track };
};

Connector.timeInfoSelector = '.ytmusic-player-bar.time-info';

Connector.isPlaying = () => {
	return Util.getAttrFromSelectors(playButtonSelector, 'd') === playingPath;
};

Connector.getUniqueID = () => {
	const videoUrl = Util.getAttrFromSelectors('.yt-uix-sessionlink', 'href');

	if (videoUrl) {
		return Util.getYtVideoIdFromUrl(videoUrl);
	}
};

Connector.isScrobblingAllowed = () => !Util.isElementVisible(adSelector);

function getArtists() {
	// FIXME Use Array.from after jQuery support will be removed
	const artistElements = Util.queryElements(artistSelectors);
	return artistElements && Util.joinArtists(artistElements.toArray());
}

function filterYoutubeIfNonAlbum(text) {
	return hasVideoAlbum() ? text : MetadataFilter.youtube(text);
}

const youtubeMusicFilter = MetadataFilter.createFilter({
	track: [
		filterYoutubeIfNonAlbum,
		MetadataFilter.removeRemastered,
		MetadataFilter.removeLive,
	],
	album: [MetadataFilter.removeRemastered, MetadataFilter.removeLive],
});

Connector.applyFilter(youtubeMusicFilter);


<|start_filename|>src/connectors/live365.js<|end_filename|>
'use strict';

Connector.trackArtSelector = "img[alt^='Art for']";

// Due to the way the website is built, I believe we can't really define a selector to access the player directly
// for this reason, I believe the track art to be the closest we can get to the player
Connector.playerSelector = Connector.trackArtSelector;

Connector.pauseButtonSelector = '.icon--pause-circle';

const artistAlbumSeparator = ' by ';

Connector.getArtistTrack = () => {
	// "Art for Layla by Derek & <NAME>"
	const artistAndTrackUnparsed = Util.getAttrFromSelectors(Connector.trackArtSelector, 'alt');

	const artistAndTrackParsed = Util.splitArtistTrack(artistAndTrackUnparsed, [artistAlbumSeparator], { swap: true });

	return artistAndTrackParsed;
};

const filter = MetadataFilter.createFilter({ track: removeTrackPrefix });
Connector.applyFilter(filter);

function removeTrackPrefix(track) {
	return track.replace('Art for ', '');
}

const LIVE_365_ARTIST = 'Live365';
const ADWTAG = 'Advert:';
const ADBREAK = 'Ad Break';

Connector.isScrobblingAllowed = () => {
	const artist = Connector.getArtistTrack().artist;
	return artist !== null
		&& artist !== LIVE_365_ARTIST
		&& artist !== 'undefined'
		&& !artist.includes(ADWTAG)
		&& !artist.includes(ADBREAK);
};

// By overriding this method we can work around the limitation of not being able to do a proper selector for the player
Connector.getObserveTarget = () => {
	// By selecting the parent of the track art, we get something very close to the player, which results in an overall better behaviour
	return document.querySelector(Connector.playerSelector).parentElement;
};


<|start_filename|>src/connectors/playirish.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({ artist: removeNowPlayingPrefix });

Connector.playerSelector = '.qt-musicplayer';

Connector.artistTrackSelector = '#streamCurrentlyPlaying';

Connector.pauseButtonSelector = '.sm2_playing';

Connector.applyFilter(filter);

function removeNowPlayingPrefix(artist) {
	return artist.replace('NOW PLAYING:', '');
}


<|start_filename|>src/connectors/stingray.js<|end_filename|>
'use strict';

Connector.playerSelector = 'player';

Connector.artistSelector =
	'cover-with-info.current.cover artist-names div.value';

Connector.trackSelector =
	'cover-with-info.current.cover div.title-content > span.title';

Connector.albumSelector = 'cover-with-info.current.cover div.album';

Connector.trackArtSelector = 'cover-with-info.current.cover div.background';

Connector.currentTimeSelector = 'time-progress div.time.duration';

Connector.durationSelector = 'time-progress div.track.duration';

Connector.isPlaying = () => {
	return Util.hasElementClass(
		'time-progress div.time.duration',
		'ng-star-inserted'
	);
};


<|start_filename|>src/connectors/redbull.js<|end_filename|>
'use strict';

const ALLOWED_TYPES = ['video.other', 'video.episode'];

const ARTIST_TRACK_GETTERS = [
	'videoTitle',
	'pageTitle',
	'description',
];

/**
 * for video episode page title in the form of
 * Programme: Track by Artist
 * eg. https://www.redbull.com/int-en/videos/episode-3-full-track-video-check-your-dms
 */
const PAGE_TITLE_REG_EXP = {
	pattern: /(.+)\sby\s([^-–]+)/,
	groups: { artist: 2, track: 1 },
};

let artistTrack = {
	artist: null,
	track: null,
};

Connector.playerSelector = '#app';

Connector.currentTimeSelector = '.rbPlyr-timeElapsed';

Connector.durationSelector = '.rbPlyr-timeDuration';

Connector.getUniqueID = () => {
	const text = Util.getAttrFromSelectors(
		'[data-content-id]', 'data-content-id');
	return text && text.split(':').slice(-2).shift();
};

Connector.getArtistTrack = () => {
	for (const getter of ARTIST_TRACK_GETTERS) {
		artistTrack = getArtistTrackFrom(getter);

		if (!Util.isArtistTrackEmpty(artistTrack)) {
			break;
		}
	}

	return artistTrack;
};

Connector.playButtonSelector = 'rbPlyr-playPause:not(.rbPlyr-pause)';

Connector.isScrobblingAllowed = () => {
	const pageType = Util.getAttrFromSelectors(
		'meta[property="og:type"]', 'content');

	const categoryLink = Util.getAttrFromSelectors(
		'a[href*="/tags/"]', 'href');

	return pageType && ALLOWED_TYPES.includes(pageType) && categoryLink && 'music' === categoryLink.split('/').pop();
};

function getArtistTrackFrom(getter) {
	// escape special characters for regExp
	const track = artistTrack.track && artistTrack.track.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');

	if (getter === 'videoTitle') {
		// Example: https://www.redbull.com/int-en/episodes/red-bull-records-the-aces-daydream
		const text = Util.getAttrFromSelectors(
			'meta[property="og:title"]', 'content');

		if (!text) {
			return artistTrack;
		}
		artistTrack = Util.splitArtistTrack(text);

		if (Util.isArtistTrackEmpty(artistTrack)) {
			artistTrack = Util.processYtVideoTitle(text);
		}

		if (Util.isArtistTrackEmpty(artistTrack)) {
			artistTrack.track = text;
		}

	} else if (getter === 'pageTitle') {
		// Example: see Line 14
		const pageTitle = Util.getTextFromSelectors('title');
		const text = pageTitle && pageTitle
			.split(':').pop()
			.replace(/( music)? video$/i, '');

		if (!text || !text.match(track)) {
			return artistTrack;
		}
		const regPage = text.match(PAGE_TITLE_REG_EXP.pattern);

		if (regPage) {
			artistTrack.artist = regPage[
				PAGE_TITLE_REG_EXP.groups.artist];
		}

		if (Util.isArtistTrackEmpty(artistTrack)) {
			const ytResult = Util.processYtVideoTitle(text);

			if (ytResult.track.match(track)) {
				artistTrack.artist = ytResult.artist;
			} else if (ytResult.artist.match(track)) {
				artistTrack.artist = ytResult.track;
			}
		}

	} else if (getter === 'description') {
		// Example: https://www.redbull.com/int-en/videos/check-your-dms-s1-e2-full-track-video
		const text = Util.getAttrFromSelectors(
			'meta[property="og:description"]', 'content');

		if (!text) {
			return artistTrack;
		}

		const regDesc = text.match(new RegExp(`(.*)${track}(.*)`));

		if (!regDesc) {
			return artistTrack;
		}

		const regResult = regDesc[2].match(/\bby\b(.*[^.])\.?/);

		if (regResult) {
			artistTrack.artist = regResult[1];
		} else if (regDesc[1]) {
			artistTrack.artist = regDesc[1].match(/(.*)\b.+\b/)[1];
		}

	}
	return artistTrack;
}


<|start_filename|>src/connectors/radio7.js<|end_filename|>
'use strict';

Connector.playerSelector = '#page-wrap';

Connector.artistTrackSelector = '#now';

Connector.pauseButtonSelector = '#jp_container_1 .jp-pause';


<|start_filename|>src/connectors/vodafone.fm.js<|end_filename|>
'use strict';

Connector.playerSelector = '.now-playing-info';

Connector.artistSelector = '#artist_name';

Connector.trackSelector = '#song_name';

Connector.playButtonSelector = '#play';

Connector.trackArtSelector = '#cover.src';


<|start_filename|>src/connectors/1001tracklists.js<|end_filename|>
'use strict';

let nextCue;
let cue;
let currentTime;
const mixDuration = Util.stringToSeconds(
	Util.getTextFromSelectors('.mediaTabItm:not(.hidden) .active')
		.split('[')[1]
		.split(']')[0]
);

Connector.playerSelector = '#playerWidgetFields';

Connector.getTrackArt = () =>
	Util.getAttrFromSelectors('.cPlay:first .artM', 'data-src');

Connector.albumSelector = '#pageTitle';

Connector.getUniqueID = () => Util.getAttrFromSelectors('.cPlay:first', 'id');

Connector.getArtistTrack = () => {
	const text = Util.getAttrFromSelectors(
		'.cPlay:first meta[itemprop="name"]',
		'content'
	);
	return Util.splitArtistTrack(text);
};

Connector.getCurrentTime = () => {
	return currentTime - cue;
};

Connector.getDuration = () => {
	switch (true) {
		case cue && nextCue > 0:
			0 > currentTime - cue && (nextCue += Math.abs(currentTime - cue));
			return nextCue - cue;
		case nextCue > 0:
			return nextCue;
		case cue > 0:
			return mixDuration - cue;
	}
};

Connector.isPlaying = () =>
	Util.hasElementClass('#playerWidgetPause', 'fa-pause');

Connector.isScrobblingAllowed = () => {
	nextCue = +Util.getAttrFromSelectors(
		'.cPlay:first ~ .tlpTog:first input',
		'value'
	);
	cue = +Util.getAttrFromSelectors('.cPlay:first input', 'value');
	currentTime = Util.getSecondsFromSelectors('#playerWidgetCurrentTime');
	const noIDs = Util.queryElements('.cPlay:first .redTxt').length;
	const mashup = Util.hasElementClass(
		'.cPlay:first span.trackValue',
		'mashupTrack'
	);

	return noIDs <= 0 && (cue > 0 || nextCue > 0) && !mashup;
};


<|start_filename|>src/connectors/r-a-d.io.js<|end_filename|>
'use strict';

const playPauseButtonSelector = '#stream-play';

Connector.artistTrackSelector = '#np';

Connector.playerSelector = '.dynamic-row > .col-md-6';

Connector.durationSelector = '#progress-length';

Connector.currentTimeSelector = '#progress-current';

Connector.isPlaying = () =>
	Util.getTextFromSelectors(playPauseButtonSelector) === 'Stop Stream';


<|start_filename|>src/connectors/globalplayer.js<|end_filename|>
'use strict';

const showNameSelector = '.show-info-with-image__text__title';

Connector.playerSelector = '.track-container';

Connector.trackSelector = '.track-container .show-info__text__title';

Connector.artistSelector = '.track-container .show-info__text__subtitle';

Connector.playButtonSelector = '.circular-play-button';

Connector.isScrobblingAllowed = () => {
	return Connector.getTrack() !== getShowName();
};

function getShowName() {
	return Util.getTextFromSelectors(showNameSelector);
}


<|start_filename|>src/connectors/truckersfm.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({
	artist: formatArtists,
});

Connector.playerSelector = '.header';

Connector.trackArtSelector = '.player-image > img';

Connector.artistSelector = '.player-artist-text';

Connector.trackSelector = '.player-title-text';

Connector.playButtonSelector = '.pause-btn > .fa-play';

Connector.onReady = Connector.onStateChanged;

Connector.applyFilter(filter);

function formatArtists(text) {
	const artist = text.split(',');
	return artist[0];
}


<|start_filename|>src/connectors/beatport.js<|end_filename|>
'use strict';

const filter = MetadataFilter.createFilter({ track: removeOriginalMix });
const playerBar = '.Player__container';

Connector.playerSelector = playerBar;

Connector.artistSelector = `${playerBar} .track-artists`;

Connector.trackSelector = `${playerBar} .track-title__primary`;

Connector.playButtonSelector = `${playerBar} #Player__play-button`;

Connector.currentTimeSelector = '.Clock__played';

Connector.durationSelector = '.Clock__total';

Connector.trackArtSelector = '.Player__artwork-2';

Connector.getUniqueID = () => {
	const trackUrl = $(`${playerBar} .track-title a`).attr('href');
	return trackUrl.split('/').pop();
};

Connector.applyFilter(filter);

function removeOriginalMix(track) {
	const remixedBy = $(`${playerBar} .track-title__remixed`).text();
	if (remixedBy === 'Original Mix') {
		return track;
	}

	return `${track} (${remixedBy})`;
}


<|start_filename|>src/connectors/wfmu.js<|end_filename|>
'use strict';

/*
 * This connector covers archived tracks only. A `radiorethink` connector
 * is used to get track info from WFMU main streams.
 */

const filter = MetadataFilter.createFilter({
	track: cleanupTrack,
});

Connector.playerSelector = '.archiveplayer';

Connector.artistSelector = '#np-artist';

Connector.trackSelector = '#np-song';

Connector.playButtonSelector = '.mejs-play';

Connector.applyFilter(filter);

function cleanupTrack(track) {
	// Extract a track title from a `"Track" by Artist` string.
	return track.replace(/(")(.*)(")( by )(.*)/g, '$2');
}


<|start_filename|>src/connectors/radiorecord.js<|end_filename|>
'use strict';

const playerBar = '.player';
const trackInfoSelector = `${playerBar} .wn-V5h-u-OET_437EyjDf`;

Connector.playerSelector = '.player';

Connector.artistSelector = `${playerBar} ._1SfzMWS_FLqI5gTeovlVHb`;

Connector.trackSelector = `${playerBar} .LvJXZ-tmO_091qVzXsrAA`;

Connector.pauseButtonSelector = '.icon-player-pause';

Connector.getUniqueID = () => {
	const trackUrl = Util.getAttrFromSelectors(trackInfoSelector, 'href');
	return trackUrl && trackUrl.split('/').pop();
};


<|start_filename|>src/core/background/pipeline/user-input.js<|end_filename|>
'use strict';

/**
 * The pipeline stage applies custom song info data to given song info.
 * Plus, it saves the song info to browser storage.
 */

define((require) => {
	const SavedEdits = require('storage/saved-edits');

	/**
	 * Fill song info by user defined values.
	 * @param  {Object} song Song instance
	 */
	async function process(song) {
		let isSongInfoLoaded = false;
		try {
			isSongInfoLoaded = await SavedEdits.loadSongInfo(song);
		} catch (e) {
			// Do nothing
		}

		song.flags.isCorrectedByUser = isSongInfoLoaded;
	}

	return { process };
});


<|start_filename|>src/connectors/eggs-dom-inject.js<|end_filename|>
'use strict';

/*
 * This script runs in non-isolated environment(eggs.mu itself)
 * for accessing window variables
 */

const isArtistPage = window.location.href.includes('/artist/');
let frameID = '';
let currentTime = 0;
let duration = 0;

let videoId = '';

if (isArtistPage) {
	const observer = new MutationObserver(toggleExternalPlayer);

	observer.observe(document.body, { childList: true });
} else {
	window.player.addEventListener('onStateChange', onYoutubeSongStateChange);
}

function toggleExternalPlayer(mutationList) {
	const removedList = mutationList[0].removedNodes;

	if (removedList.length) {
		// external player has been started
		if (removedList[0].id === 'fancybox-loading') {
			replaceYoutubeVideo();
			return null;
		}
		// external player has been closed
		if (removedList[0].classList.contains('fancybox-overlay')) {
			onYoutubeClose();
			return null;
		}
	}
}

function replaceYoutubeVideo() {
	const videoFrame = document.querySelector('.fancybox-inner iframe');
	videoId = videoFrame.src.split('/').pop().split('?')[0];

	videoFrame.src += '&enablejsapi=1&widgetid=1';
	frameID = videoFrame.id;

	videoFrame.addEventListener('load', function() {
		let message = JSON.stringify({
			event: 'listening',
			id: frameID,
			channel: 'widget',
		});
		videoFrame.contentWindow.postMessage(message, 'https://www.youtube.com');

		message = JSON.stringify({
			event: 'command',
			func: 'addEventListener',
			args: ['onStateChange'],
			id: frameID,
			channel: 'widget',
		});
		videoFrame.contentWindow.postMessage(message, 'https://www.youtube.com');
	});
}

window.addEventListener('message', (event) => {
	if (event.origin !== 'https://www.youtube.com') {
		return;
	}
	const data = JSON.parse(event.data);
	switch (data.event) {
		case 'onStateChange':
			onYoutubeStateChange(data);
			break;
		case 'infoDelivery':
			getTimestamps(data);
			break;
	}
});

function onYoutubeStateChange(data) {
	const currentPlayer = document.querySelector(`a[href*="${videoId}"]`);
	const parentElmt = (currentPlayer && currentPlayer.closest('li')) || document;
	const playerTypeSuffix = (data.info === -1) ? 'start' : '';

	window.postMessage({
		sender: 'web-scrobbler',
		playerType: `youtube${playerTypeSuffix}`,
		isPlaying: data.info === 1,
		timeInfo: {
			currentTime: currentTime || 0,
			duration,
		},
		trackInfo: {
			artist: parentElmt.querySelector(`.artist_name${(isArtistPage) ? '' : ' a'}`).textContent,
			track: parentElmt.querySelector(`.product_name${(isArtistPage) ? ' a' : ' p'}`).textContent,
		},
	}, '*');
}

function onYoutubeClose() {
	const currentPlayer = document.querySelector(`a[href*="${videoId}"]`);
	const parentElmt = (currentPlayer && currentPlayer.closest('li')) || document;
	window.postMessage({
		sender: 'web-scrobbler',
		playerType: 'youtube',
		isPlaying: false,
		timeInfo: {
			currentTime,
			duration,
		},
		trackInfo: {
			artist: parentElmt.querySelector(`.artist_name${(isArtistPage) ? '' : ' a'}`).textContent,
			track: parentElmt.querySelector(`.product_name${(isArtistPage) ? ' a' : ' p'}`).textContent,
		},
	}, '*');
}

function onYoutubeSongStateChange(event) {
	const currentPlayer = document.querySelector(`a[href*="${videoId}"]`);
	const parentElmt = (currentPlayer && currentPlayer.closest('li')) || document;
	const playerTypeSuffix = (event.data === -1) ? 'start' : '';

	window.postMessage({
		sender: 'web-scrobbler',
		playerType: `youtube${playerTypeSuffix}`,
		isPlaying: event.data === 1,
		timeInfo: {
			currentTime: event.target.getCurrentTime(),
			duration: event.target.getDuration(),
		},
		trackInfo: {
			artist: parentElmt.querySelector(`.artist_name${(isArtistPage) ? '' : ' a'}`).textContent,
			track: parentElmt.querySelector(`.product_name${(isArtistPage) ? ' a' : ' p'}`).textContent,
		},
	}, '*');
}

function getTimestamps(data) {
	if (data.info) {
		if (data.info.currentTime) {
			currentTime = data.info.currentTime;
		}
		if (data.info.duration) {
			duration = data.info.duration;
		}
	}
}


<|start_filename|>src/connectors/migu-music.js<|end_filename|>
'use strict';

Connector.playerSelector = '.mod-player';

Connector.trackSelector = '.song a';

Connector.artistSelector = '.singer a';

Connector.albumSelector = '.album a';

Connector.playButtonSelector = '.cf-player-play';

Connector.timeInfoSelector = '.current-time';

Connector.trackArtSelector = '.song-cover img';

Connector.isTrackArtDefault = (trackArtUrl) => {
	return trackArtUrl.includes('data:image/jpeg;base64');
};

Connector.getUniqueID = () => {
	return Util.getAttrFromSelectors('.J_OrderLink', 'data-id');
};


<|start_filename|>src/connectors/xteam-radio.js<|end_filename|>
'use strict';

Connector.playerSelector = '.now-playing-bar--container';

Connector.pauseButtonSelector = '.play-pause-button--pause-container';

Connector.trackSelector =	'.song-info--container.now-playing-bar--song-info > .song-info--name';

Connector.artistSelector = '.song-info--container.now-playing-bar--song-info > .song-info--artist-name';

Connector.albumSelector =	'.song-info--container.now-playing-bar--song-info > .song-info--album-name';


<|start_filename|>src/connectors/sndtst.js<|end_filename|>
'use strict';

Connector.playerSelector = 'body';

Connector.artistSelector = 'h1';

Connector.trackSelector = '.jp-title';

Connector.pauseButtonSelector = '.jp-pause';


<|start_filename|>src/connectors/music-flo.js<|end_filename|>
'use strict';

Connector.playerSelector = '.player_ct.mini';
Connector.artistSelector = '.track_area .track_info .artist';
Connector.trackSelector = '.track_area .track_info .title';
Connector.pauseButtonSelector = '.control_area .icon-player.btn-player-pause';
Connector.trackArtSelector = '.playbar_ct .thumb';
Connector.durationSelector = '.time_all > span';
Connector.currentTimeSelector = '.time_current > span';


<|start_filename|>src/connectors/ragya.js<|end_filename|>
'use strict';

Connector.playerSelector = '.music-box';

Connector.trackSelector = '.raga-name';

Connector.artistSelector = '.artist-name';

Connector.albumSelector = '.album-name';

Connector.currentTimeSelector = '.timer .clr-primary';

Connector.durationSelector = '.timer .clr-white';

Connector.isPlaying = () => document.querySelector('.play-btn-logo').getAttribute('src') === 'assets/images/pause.svg';


<|start_filename|>src/connectors/magnatune.js<|end_filename|>
'use strict';

const artistSelector = "a[href^='/artists']";

const trackRegex = /(.+)\[(\.)?/;

Connector.playerSelector = '#paper';

Connector.artistSelector = artistSelector;

Connector.getTrack = () => {
	const playingImg = document.querySelector('#songlist img');

	if (!playingImg) {
		return null;
	}

	const trackContent = playingImg.closest('td').nextElementSibling.textContent;
	const matched = trackContent.match(trackRegex);
	if (!matched) {
		return null;
	}

	return matched[1];
};

Connector.getAlbum = () => {
	const artistElement = document.querySelector(artistSelector);

	return artistElement.closest('font').textContent.replace(artistElement.textContent, '').slice(1);
};

Connector.isPlaying = () => document.querySelectorAll('#hugeplay a').length === 3;


<|start_filename|>src/connectors/archive.js<|end_filename|>
'use strict';

/**
 * Example links to debug and test the connector:
 *
 * https://archive.org/details/AH013_sarin_sunday_-_the_lonely_hike
 * Full album
 *
 * https://archive.org/details/AH003_corwin_trails_-_corwin_trails
 * Full album with numeric prefixes
 *
 * https://archive.org/details/lp_everybody-knows-this-is-nowhere_neil-young-crazy-horse-robin-lane
 * Full album with artist suffixed in track names
 *
 * https://archive.org/details/dont-lie-beets-produce-ny-mix
 * Single track
 */

const artistSelectors = [
	'.key-val-big a span',
	'.item-details-metadata > dl > dd a',
];
const trackSelectors = [
	// https://archive.org/details/AH003_corwin_trails_-_corwin_trails
	'.jwrowV2.playing .ttl',

	// https://archive.org/details/lp_everybody-knows-this-is-nowhere_neil-young-crazy-horse-robin-lane
	'.audio-track-list .selected .track-title',
];
const albumSelector = '.thats-left > h1 [itemprop=name]';
const tracksSelector = '.jwrowV2 .ttl';

const numericTrackRegex = /^\d+\w+/;

const filter = MetadataFilter.createFilter({ track: removeNumericPrefixes });

Connector.applyFilter(filter);

function removeNumericPrefixes(track) {
	if (hasAllTracksNumericPrefix()) {
		return track.trim().replace(/^(\d+\w+)/, '');
	}

	return track;
}

Connector.currentTimeSelector = '.jw-text-elapsed';

Connector.durationSelector = '.jw-text-duration';

Connector.isPlaying = () => {
	const videoElement = document.querySelector('video');

	if (videoElement === null) {
		return false;
	}

	return !videoElement.paused;
};

Connector.playerSelector = '#theatre-ia';

Connector.trackArtSelector = [
	// https://archive.org/details/AH013_sarin_sunday_-_the_lonely_hike
	'#theatre-ia center > img',

	// https://archive.org/details/lp_everybody-knows-this-is-nowhere_neil-young-crazy-horse-robin-lane
	'.album-cover img',
];

Connector.getTrackInfo = () => {
	const artist = getArtists(artistSelectors);
	let album = Util.getTextFromSelectors(albumSelector);
	let track = Util.getTextFromSelectors(trackSelectors);

	if (track) {
		// Second item could be an artist name or track duration
		const [firstItem, secondItem] = track
			.split('-')
			.map((item) => item.trim());

		if (artist.includes(secondItem)) {
			track = firstItem;
		}
	} else {
		track = album;
		album = null;
	}

	return { artist, track, album };
};

function hasAllTracksNumericPrefix() {
	const tracks = document.querySelectorAll(tracksSelector);
	if (tracks.length === 0) {
		return false;
	}

	let hasAllTracksNumericPrefix = true;
	for (const track of tracks) {
		if (!numericTrackRegex.test(track.textContent.trim())) {
			hasAllTracksNumericPrefix = false;
			break;
		}
	}

	return hasAllTracksNumericPrefix;
}

function getArtists(selectors) {
	const artistElements = Util.queryElements(selectors);
	return artistElements && Util.joinArtists(artistElements.toArray());
}


<|start_filename|>src/connectors/synology.js<|end_filename|>
'use strict';

Connector.playerSelector = '.syno-as-control';

Connector.trackSelector = '.info-title';

Connector.getTrackInfo = () => {
	const albumArtistText = document.querySelector('.info-album-artist').textContent;
	return Util.splitArtistAlbum(albumArtistText, null, { swap: true });
};

Connector.currentTimeSelector = '.info-position';

Connector.durationSelector = '.info-duration';

Connector.trackArtSelector = '.player-info-thumb';

Connector.isPlaying = () => Util.hasElementClass('.player-play > span', 'player-btn-pause');


<|start_filename|>src/connectors/subfire.js<|end_filename|>
'use strict';

Connector.playerSelector = '#player-cover';

Connector.getArtist = () => document.querySelector('input#artist').value;

Connector.getTrack = () => document.querySelector('input#title').value;

Connector.getAlbum = () => document.querySelector('input#album').value;

Connector.playButtonSelector = '#player-cover #player-btn-playpause.ui-icon-play';

Connector.trackArtSelector = '#player-cover img#coverart';

// original selectors are `#duration-label .currentTime` and `#duration-label .endTime`
// but these values are pretty unstable during loading time
Connector.getCurrentTime = () => document.querySelector('#player').currentTime;

Connector.getDuration = () => document.querySelector('#player').duration;

Connector.getUniqueID = () => {
	const src = document.querySelector('#player').src;
	return src && src.match(/id=([^&]*)/i)[1] || null;
};

Connector.isScrobblingAllowed = () => Util.isElementVisible(Connector.playerSelector);


<|start_filename|>src/ui/popups/go-play-music.css<|end_filename|>
.description {
	margin-top: 1rem;
}

.help {
	font-size: 0.9em;
}


<|start_filename|>src/connectors/winampify.js<|end_filename|>
'use strict';

Connector.playerSelector = '#winamp-meta';
Connector.playButtonSelector = '#main-window .play';
Connector.albumSelector = '#winamp-meta-album';
Connector.artistSelector = '#winamp-meta-artist';
Connector.trackSelector = '#winamp-meta-title';
