<commit_before>'use strict';

const escapeHtmlEntityMap = {
	'&': /&/g,
	'<': /</g,
	'>': />/g,
	'"': /"/g,
};

/**
 * Base filter object that filters metadata fields by given filter set.
 * A filter set is an object containing 'artist', 'track', 'album' or 'all'
 * properties. Each property can be defined either as a filter function or as
 * an array of filter functions. The 'artist', 'track' and 'album' properties
 * are used to define functions to filter artist, track and album metadata
 * fields respectively. The 'all' property can be used to define common filter
 * functions for all metadata fields.
 *
 * Filter function is a function which takes non-null string argument
 * and returns modified string.
 */
class MetadataFilter {
	/**
	 * @constructor
     * @param {Object} filterSet Set of filters
     */
	constructor(filterSet) {
		this.mergedFilterSet = {};
		this.appendFilters(filterSet);
	}

	/**
	 * Filter text using filters for artist metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterArtist(text) {
		return this.filterText(text, this.mergedFilterSet.artist);
	}

	/**
	 * Filter text using filters for track metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterTrack(text) {
		return this.filterText(text, this.mergedFilterSet.track);
	}

	/**
	 * Filter text using filters for album metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterAlbum(text) {
		return this.filterText(text, this.mergedFilterSet.album);
	}

	/**
	 * Filter field using filters for given field.
	 * @param  {String} field Metadata field
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterField(field, text) {
		return this.filterText(text, this.mergedFilterSet[field]);
	}

	/**
	 * Append new filter set to existing filters.
	 * @param  {Object} filterSet Set of filters
	 * @return {MetadataFilter} Current instance
	 */
	append(filterSet) {
		this.appendFilters(filterSet);
		return this;
	}

	/**
	 * Extend filter set by filter set from given filter.
	 * @param  {Object} filter Filter object
	 * @return {MetadataFilter} Current instance
	 */
	extend(filter) {
		this.appendFilters(filter.mergedFilterSet);
		return this;
	}

	/**
	 * Internal.
	 */

	/**
	 * Filter text using given filters.
	 * @param  {String} text String to be filtered
	 * @param  {Array} filters Array of filter functions
	 * @return {String} Filtered string
	 */
	filterText(text, filters) {
		if (!text) {
			return text;
		}

		for (let filter of filters) {
			text = filter(text);
		}

		return text;
	}

	/**
	 * Convert given filters into array of filters.
	 * @param  {Any} filters Array of filter functions or filter function
	 * @return {Array} Array of filter funcions
	 */
	createFilters(filters) {
		if (typeof filters === 'function') {
			let filterFunction = filters;
			return [filterFunction];
		} else if (Array.isArray(filters)) {
			return filters;
		}

		throw new Error(`Invalid filter type: ${typeof filters}`);
	}

	/**
	 * Add given filters to current ones.
     * @param {Object} filterSet Set of filters
	 */
	appendFilters(filterSet) {
		for (let field of ['artist', 'track', 'album']) {
			if (this.mergedFilterSet[field] === undefined) {
				this.mergedFilterSet[field] = [];
			}

			if (filterSet[field]) {
				this.mergedFilterSet[field] = this.mergedFilterSet[field]
					.concat(this.createFilters(filterSet[field]));
			}

			if (filterSet.all) {
				this.mergedFilterSet[field] = this.mergedFilterSet[field]
					.concat(this.createFilters(filterSet.all));
			}
		}
	}

	/**
	 * Predefined filter functions.
	 */

	/**
	 * Trim given string.
	 * @param  {String} text String to be trimmed
	 * @return {String}	Trimmed string
	 */
	static trim(text) {
		return text.trim();
	}

	/**
	 * Remove zero-width characters from given string.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeZeroWidth(text) {
		return text.replace(/[\u200B-\u200D\uFEFF]/g, '');
	}

	/**
	 * Decode HTML entities in given text string.
	 * @param  {String} text String with HTML entities
	 * @return {String} Decoded string
	 */
	static decodeHtmlEntities(text) {
		for (let target in escapeHtmlEntityMap) {
			let source = escapeHtmlEntityMap[target];
			text = text.replace(source, target);
		}

		text = text.replace(/&#x([a-fA-f0-9]+);/g, (match, hex) => {
			let dec = parseInt(hex, 16);
			return String.fromCharCode(dec);
		});
		text = text.replace(/&#(\d+);/g, (match, dec) => {
			return String.fromCharCode(dec);
		});

		return text;
	}

	/**
	 * Remove Youtube-related garbage from the text.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static youtube(text) {
		return MetadataFilter.filterWithFilterSet(
			text, MetadataFilter.YOUTUBE_TRACK_FILTERS
		);
	}

	/**
	 * Remove "Remastered..."-like strings from the text.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeRemastered(text) {
		return MetadataFilter.filterWithFilterSet(
			text, MetadataFilter.REMASTERED_FILTERS
		);
	}

	/**
	 * Replace text according to given filter set rules.
	 * @param  {String} text String to be filtered
	 * @param  {Object} set  Array of replace rules
	 * @return {String} Filtered string
	 */
	static filterWithFilterSet(text, set) {
		for (let data of set) {
			text = text.replace(data.source, data.target);
		}

		return text;
	}

	/**
	 * The filter set is an array that contains replace rules.
	 *
	 * Each rule is an object that contains 'source' and 'target' properties.
	 * 'Source' property is a string or RegEx object which is replaced by
	 * 'target' property value.
	 */

	/**
	 * Predefined regex-based filter set for Youtube-based connectors.
	 * @type {Array}
	 */
	static get YOUTUBE_TRACK_FILTERS() {
		return [
			// Trim whitespaces
			{ source: /^\s+|\s+$/g, target: '' },
			// **NEW**
			{ source: /\s*\*+\s?\S+\s?\*+$/, target: '' },
			// [whatever]
			{ source: /\s*\[[^\]]+\]$/, target: '' },
			// (whatever version)
			{ source: /\s*\([^)]*version\)$/i, target: '' },
			// video extensions
			{ source: /\s*\.(avi|wmv|mpg|mpeg|flv)$/i, target: '' },
			// (LYRICs VIDEO)
			{ source: /\s*((with)?\s*lyrics?( video)?\s*)/i, target: '' },
			// (Official Track Stream)
			{ source: /\s*(Official Track Stream*)/i, target: '' },
			// (official)? (music)? video
			{ source: /\s*(of+icial\s*)?(music\s*)?video/i, target: '' },
			// (official)? (music)? audio
			{ source: /\s*(of+icial\s*)?(music\s*)?audio/i, target: '' },
			// (ALBUM TRACK)
			{ source: /\s*(ALBUM TRACK\s*)?(album track\s*)/i, target: '' },
			// (Cover Art)
			{ source: /\s*(COVER ART\s*)?(Cover Art\s*)/i, target: '' },
			// (official)
			{ source: /\s*\(\s*of+icial\s*\)/i, target: '' },
			// (1999)
			{ source: /\s*\(\s*[0-9]{4}\s*\)/i, target: '' },
			// HD (HQ)
			{ source: /\s+\(\s*(HD|HQ)\s*\)$/, target: '' },
			// HD (HQ)
			{ source: /\s+(HD|HQ)\s*$/, target: '' },
			// video clip officiel
			{ source: /\s*(vid[\u00E9e]o)?\s*clip officiel/i, target: '' },
			// offizielles
			{ source: /\s*of+iziel+es\s*/i, target: '' },
			// video clip
			{ source: /\s*(vid[\u00E9e]o)?\s*clip/i, target: '' },
			// Full Album
			{ source: /\s*full\s*album/i, target: '' },
			// live
			{ source: /\s+\(?live\)?$/i, target: '' },
			// Leftovers after e.g. (official video)
			{ source: /\(+\s*\)+/, target: '' },
			// Artist - The new "Track title" featuring someone
			{ source: /^(|.*\s)"(.{5,})"(\s.*|)$/, target: '$2' },
			// 'Track title'
			{ source: /^(|.*\s)'(.{5,})'(\s.*|)$/, target: '$2' },
			// (*01/01/1999*)
			{ source: /\s*\(.*[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}.*\)/i, target: '' },

			// labels
			{ source: /\|\sNapalm\sRecords$/, target: '' },

			// trim starting white chars and dash
			{ source: /^[/,:;~-\s"]+/, target: '' },
			// trim trailing white chars and dash
			{ source: /[/,:;~-\s"!]+$/, target: '' },
		];
	}

	/**
	 * A regex-based filter set that contains removal rules of "Remastered..."-like
	 * strings from a text. Used by Spotify and Deezer connectors.
	 * @type {Array}
	 */
	static get REMASTERED_FILTERS() {
		return [
			// Here Comes The Sun - Remastered
			{ source: /-\sRemastered$/, target: '' },
			// Hey Jude - Remastered 2015
			{ source: /-\sRemastered\s\d+$/, target: '' },
			// Let It Be (Remastered 2009)
			{ source: /\(Remastered\s\d+\)$/, target: '' },
			// Pigs On The Wing (Part One) [2011 - Remaster]
			{ source: /\[\d+\s-\sRemaster\]$/, target: '' },
			// Comfortably Numb (2011 - Remaster)
			{ source: /\(\d+\s-\sRemaster\)$/, target: '' },
			// Outside The Wall - 2011 - Remaster
			{ source: /-\s\d+\s-\sRemaster$/, target: '' },
			// Learning To Fly - 2001 Digital Remaster
			{ source: /-\s\d+\s.+?\sRemaster$/, target: '' },
			// Your Possible Pasts - 2011 Remastered Version
			{ source: /-\s\d+\sRemastered Version$/, target: '' },
			// Roll Over Beethoven (Live / Remastered)
			{ source: /\(Live\s\/\sRemastered\)$/i, target: '' },
			// Ticket To Ride - Live / Remastered
			{ source: /-\sLive\s\/\sRemastered$/, target: '' },
			// Mothership (Remastered)
			// How The West Was Won [Remastered]
			{ source: /[([]Remastered[)\]]$/, target: '' },
		];
	}

	/**
	 * Get simple trim filter object used by default in a Connector object.
	 * @return {MetadataFilter} Filter object
	 */
	static getTrimFilter() {
		return new MetadataFilter({
			artist: MetadataFilter.trim,
			track: MetadataFilter.trim,
			album: MetadataFilter.trim
		});
	}

	/**
	 * Get predefined filter object for Youtube-based connectors.
	 * @return {MetadataFilter} Filter object
	 */
	static getYoutubeFilter() {
		return new MetadataFilter({
			track: MetadataFilter.youtube,
			all: MetadataFilter.trim
		});
	}

	/**
	 * Get predefined filter object that uses 'removeRemastered' function.
	 * @return {MetadataFilter} Filter object
	 */
	static getRemasteredFilter() {
		return new MetadataFilter({
			all: MetadataFilter.trim,
			track: MetadataFilter.removeRemastered,
			album: MetadataFilter.removeRemastered
		});
	}

	/**
	 * Get filter that replaces NBSP with space.
	 * TODO: redo to more generic filter which does some other cleaning things.
	 * @return {MetadataFilter} Filter object
	 */
	static getNbspFilter() {
		return new MetadataFilter({
			all: (text) => text.replace(' ', ' ')
		});
	}

	/**
	 * "REAL_TITLE : REAL_TILE" -> "REAL_TITLE"
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeDoubleTitle(text) {
		const splitted = text.split(' : ');
		if (splitted.length !== 2 || splitted[0] !== splitted[1]) {
			return text;
		}
		return splitted[0];
	}

	/**
	 * Get predefined filter object that uses 'removeDoubleTitle' function.
	 * @return {MetadataFilter} Filter object
	 */
	static getDoubleTitleFilter() {
		return new MetadataFilter({
			track: MetadataFilter.removeDoubleTitle,
		});
	}
}

/**
 * Export MetadataFilter constructor if script is run in Node.js context.
 */
if (typeof module !== 'undefined') {
	module.exports = MetadataFilter;
}
<commit_msg>Replace NBSP and whitespace symbols with Unicode escape sequences<commit_after>'use strict';

const escapeHtmlEntityMap = {
	'&': /&/g,
	'<': /</g,
	'>': />/g,
	'"': /"/g,
};

/**
 * Base filter object that filters metadata fields by given filter set.
 * A filter set is an object containing 'artist', 'track', 'album' or 'all'
 * properties. Each property can be defined either as a filter function or as
 * an array of filter functions. The 'artist', 'track' and 'album' properties
 * are used to define functions to filter artist, track and album metadata
 * fields respectively. The 'all' property can be used to define common filter
 * functions for all metadata fields.
 *
 * Filter function is a function which takes non-null string argument
 * and returns modified string.
 */
class MetadataFilter {
	/**
	 * @constructor
     * @param {Object} filterSet Set of filters
     */
	constructor(filterSet) {
		this.mergedFilterSet = {};
		this.appendFilters(filterSet);
	}

	/**
	 * Filter text using filters for artist metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterArtist(text) {
		return this.filterText(text, this.mergedFilterSet.artist);
	}

	/**
	 * Filter text using filters for track metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterTrack(text) {
		return this.filterText(text, this.mergedFilterSet.track);
	}

	/**
	 * Filter text using filters for album metadata field.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterAlbum(text) {
		return this.filterText(text, this.mergedFilterSet.album);
	}

	/**
	 * Filter field using filters for given field.
	 * @param  {String} field Metadata field
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	filterField(field, text) {
		return this.filterText(text, this.mergedFilterSet[field]);
	}

	/**
	 * Append new filter set to existing filters.
	 * @param  {Object} filterSet Set of filters
	 * @return {MetadataFilter} Current instance
	 */
	append(filterSet) {
		this.appendFilters(filterSet);
		return this;
	}

	/**
	 * Extend filter set by filter set from given filter.
	 * @param  {Object} filter Filter object
	 * @return {MetadataFilter} Current instance
	 */
	extend(filter) {
		this.appendFilters(filter.mergedFilterSet);
		return this;
	}

	/**
	 * Internal.
	 */

	/**
	 * Filter text using given filters.
	 * @param  {String} text String to be filtered
	 * @param  {Array} filters Array of filter functions
	 * @return {String} Filtered string
	 */
	filterText(text, filters) {
		if (!text) {
			return text;
		}

		for (let filter of filters) {
			text = filter(text);
		}

		return text;
	}

	/**
	 * Convert given filters into array of filters.
	 * @param  {Any} filters Array of filter functions or filter function
	 * @return {Array} Array of filter funcions
	 */
	createFilters(filters) {
		if (typeof filters === 'function') {
			let filterFunction = filters;
			return [filterFunction];
		} else if (Array.isArray(filters)) {
			return filters;
		}

		throw new Error(`Invalid filter type: ${typeof filters}`);
	}

	/**
	 * Add given filters to current ones.
     * @param {Object} filterSet Set of filters
	 */
	appendFilters(filterSet) {
		for (let field of ['artist', 'track', 'album']) {
			if (this.mergedFilterSet[field] === undefined) {
				this.mergedFilterSet[field] = [];
			}

			if (filterSet[field]) {
				this.mergedFilterSet[field] = this.mergedFilterSet[field]
					.concat(this.createFilters(filterSet[field]));
			}

			if (filterSet.all) {
				this.mergedFilterSet[field] = this.mergedFilterSet[field]
					.concat(this.createFilters(filterSet.all));
			}
		}
	}

	/**
	 * Predefined filter functions.
	 */

	/**
	 * Trim given string.
	 * @param  {String} text String to be trimmed
	 * @return {String}	Trimmed string
	 */
	static trim(text) {
		return text.trim();
	}

	/**
	 * Remove zero-width characters from given string.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeZeroWidth(text) {
		return text.replace(/[\u200B-\u200D\uFEFF]/g, '');
	}

	/**
	 * Decode HTML entities in given text string.
	 * @param  {String} text String with HTML entities
	 * @return {String} Decoded string
	 */
	static decodeHtmlEntities(text) {
		for (let target in escapeHtmlEntityMap) {
			let source = escapeHtmlEntityMap[target];
			text = text.replace(source, target);
		}

		text = text.replace(/&#x([a-fA-f0-9]+);/g, (match, hex) => {
			let dec = parseInt(hex, 16);
			return String.fromCharCode(dec);
		});
		text = text.replace(/&#(\d+);/g, (match, dec) => {
			return String.fromCharCode(dec);
		});

		return text;
	}

	/**
	 * Remove Youtube-related garbage from the text.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static youtube(text) {
		return MetadataFilter.filterWithFilterSet(
			text, MetadataFilter.YOUTUBE_TRACK_FILTERS
		);
	}

	/**
	 * Remove "Remastered..."-like strings from the text.
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeRemastered(text) {
		return MetadataFilter.filterWithFilterSet(
			text, MetadataFilter.REMASTERED_FILTERS
		);
	}

	/**
	 * Replace text according to given filter set rules.
	 * @param  {String} text String to be filtered
	 * @param  {Object} set  Array of replace rules
	 * @return {String} Filtered string
	 */
	static filterWithFilterSet(text, set) {
		for (let data of set) {
			text = text.replace(data.source, data.target);
		}

		return text;
	}

	/**
	 * The filter set is an array that contains replace rules.
	 *
	 * Each rule is an object that contains 'source' and 'target' properties.
	 * 'Source' property is a string or RegEx object which is replaced by
	 * 'target' property value.
	 */

	/**
	 * Predefined regex-based filter set for Youtube-based connectors.
	 * @type {Array}
	 */
	static get YOUTUBE_TRACK_FILTERS() {
		return [
			// Trim whitespaces
			{ source: /^\s+|\s+$/g, target: '' },
			// **NEW**
			{ source: /\s*\*+\s?\S+\s?\*+$/, target: '' },
			// [whatever]
			{ source: /\s*\[[^\]]+\]$/, target: '' },
			// (whatever version)
			{ source: /\s*\([^)]*version\)$/i, target: '' },
			// video extensions
			{ source: /\s*\.(avi|wmv|mpg|mpeg|flv)$/i, target: '' },
			// (LYRICs VIDEO)
			{ source: /\s*((with)?\s*lyrics?( video)?\s*)/i, target: '' },
			// (Official Track Stream)
			{ source: /\s*(Official Track Stream*)/i, target: '' },
			// (official)? (music)? video
			{ source: /\s*(of+icial\s*)?(music\s*)?video/i, target: '' },
			// (official)? (music)? audio
			{ source: /\s*(of+icial\s*)?(music\s*)?audio/i, target: '' },
			// (ALBUM TRACK)
			{ source: /\s*(ALBUM TRACK\s*)?(album track\s*)/i, target: '' },
			// (Cover Art)
			{ source: /\s*(COVER ART\s*)?(Cover Art\s*)/i, target: '' },
			// (official)
			{ source: /\s*\(\s*of+icial\s*\)/i, target: '' },
			// (1999)
			{ source: /\s*\(\s*[0-9]{4}\s*\)/i, target: '' },
			// HD (HQ)
			{ source: /\s+\(\s*(HD|HQ)\s*\)$/, target: '' },
			// HD (HQ)
			{ source: /\s+(HD|HQ)\s*$/, target: '' },
			// video clip officiel
			{ source: /\s*(vid[\u00E9e]o)?\s*clip officiel/i, target: '' },
			// offizielles
			{ source: /\s*of+iziel+es\s*/i, target: '' },
			// video clip
			{ source: /\s*(vid[\u00E9e]o)?\s*clip/i, target: '' },
			// Full Album
			{ source: /\s*full\s*album/i, target: '' },
			// live
			{ source: /\s+\(?live\)?$/i, target: '' },
			// Leftovers after e.g. (official video)
			{ source: /\(+\s*\)+/, target: '' },
			// Artist - The new "Track title" featuring someone
			{ source: /^(|.*\s)"(.{5,})"(\s.*|)$/, target: '$2' },
			// 'Track title'
			{ source: /^(|.*\s)'(.{5,})'(\s.*|)$/, target: '$2' },
			// (*01/01/1999*)
			{ source: /\s*\(.*[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}.*\)/i, target: '' },

			// labels
			{ source: /\|\sNapalm\sRecords$/, target: '' },

			// trim starting white chars and dash
			{ source: /^[/,:;~-\s"]+/, target: '' },
			// trim trailing white chars and dash
			{ source: /[/,:;~-\s"!]+$/, target: '' },
		];
	}

	/**
	 * A regex-based filter set that contains removal rules of "Remastered..."-like
	 * strings from a text. Used by Spotify and Deezer connectors.
	 * @type {Array}
	 */
	static get REMASTERED_FILTERS() {
		return [
			// Here Comes The Sun - Remastered
			{ source: /-\sRemastered$/, target: '' },
			// Hey Jude - Remastered 2015
			{ source: /-\sRemastered\s\d+$/, target: '' },
			// Let It Be (Remastered 2009)
			{ source: /\(Remastered\s\d+\)$/, target: '' },
			// Pigs On The Wing (Part One) [2011 - Remaster]
			{ source: /\[\d+\s-\sRemaster\]$/, target: '' },
			// Comfortably Numb (2011 - Remaster)
			{ source: /\(\d+\s-\sRemaster\)$/, target: '' },
			// Outside The Wall - 2011 - Remaster
			{ source: /-\s\d+\s-\sRemaster$/, target: '' },
			// Learning To Fly - 2001 Digital Remaster
			{ source: /-\s\d+\s.+?\sRemaster$/, target: '' },
			// Your Possible Pasts - 2011 Remastered Version
			{ source: /-\s\d+\sRemastered Version$/, target: '' },
			// Roll Over Beethoven (Live / Remastered)
			{ source: /\(Live\s\/\sRemastered\)$/i, target: '' },
			// Ticket To Ride - Live / Remastered
			{ source: /-\sLive\s\/\sRemastered$/, target: '' },
			// Mothership (Remastered)
			// How The West Was Won [Remastered]
			{ source: /[([]Remastered[)\]]$/, target: '' },
		];
	}

	/**
	 * Get simple trim filter object used by default in a Connector object.
	 * @return {MetadataFilter} Filter object
	 */
	static getTrimFilter() {
		return new MetadataFilter({
			artist: MetadataFilter.trim,
			track: MetadataFilter.trim,
			album: MetadataFilter.trim
		});
	}

	/**
	 * Get predefined filter object for Youtube-based connectors.
	 * @return {MetadataFilter} Filter object
	 */
	static getYoutubeFilter() {
		return new MetadataFilter({
			track: MetadataFilter.youtube,
			all: MetadataFilter.trim
		});
	}

	/**
	 * Get predefined filter object that uses 'removeRemastered' function.
	 * @return {MetadataFilter} Filter object
	 */
	static getRemasteredFilter() {
		return new MetadataFilter({
			all: MetadataFilter.trim,
			track: MetadataFilter.removeRemastered,
			album: MetadataFilter.removeRemastered
		});
	}

	/**
	 * Get filter that replaces NBSP with space.
	 * TODO: redo to more generic filter which does some other cleaning things.
	 * @return {MetadataFilter} Filter object
	 */
	static getNbspFilter() {
		return new MetadataFilter({
			all: (text) => text.replace('\u00a0', '\u0020')
		});
	}

	/**
	 * "REAL_TITLE : REAL_TILE" -> "REAL_TITLE"
	 * @param  {String} text String to be filtered
	 * @return {String} Filtered string
	 */
	static removeDoubleTitle(text) {
		const splitted = text.split(' : ');
		if (splitted.length !== 2 || splitted[0] !== splitted[1]) {
			return text;
		}
		return splitted[0];
	}

	/**
	 * Get predefined filter object that uses 'removeDoubleTitle' function.
	 * @return {MetadataFilter} Filter object
	 */
	static getDoubleTitleFilter() {
		return new MetadataFilter({
			track: MetadataFilter.removeDoubleTitle,
		});
	}
}

/**
 * Export MetadataFilter constructor if script is run in Node.js context.
 */
if (typeof module !== 'undefined') {
	module.exports = MetadataFilter;
}
<|endoftext|>


<commit_before>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Observable } from "rxjs/Observable";

import config from "../../config.js";
import log from "../../utils/log";
import {
  KeySystemAccess,
  requestMediaKeySystemAccess,
  shouldRenewMediaKeys,
} from "../../compat";

import {
  EncryptedMediaError,
} from "../../errors";

const {
  EME_DEFAULT_WIDEVINE_ROBUSTNESSES,
  EME_KEY_SYSTEMS,
} = config;

function getCachedKeySystemAccess(keySystems, instanceInfos = {}) {
  const {
    $keySystem,
    $mediaKeys,
    $mediaKeySystemConfiguration,
  }  = instanceInfos;

  // NOTE(pierre): alwaysRenew flag is used for IE11 which require the
  // creation of a new MediaKeys instance for each session creation
  if (!$keySystem || !$mediaKeys || shouldRenewMediaKeys()) {
    return null;
  }

  const configuration = $mediaKeySystemConfiguration;
  const foundKeySystem = keySystems.filter((ks) => {
    if (ks.type !== $keySystem.type) {
      return false;
    }

    if (ks.persistentLicense &&
        configuration.persistentState != "required") {
      return false;
    }

    if (ks.distinctiveIdentifierRequired &&
        configuration.distinctiveIdentifier != "required") {
      return false;
    }

    return true;
  })[0];

  if (foundKeySystem) {
    return {
      keySystem: foundKeySystem,
      keySystemAccess: new KeySystemAccess(
        $keySystem,
        $mediaKeys,
        $mediaKeySystemConfiguration),
    };
  }
  else {
    return null;
  }
}

/**
* Find key system canonical name from key system type.
* @param {string} ksType - Obtained via inversion
* @returns {string} - Either the canonical name, or undefined.
*/
function findKeySystemCanonicalName(ksType) {
  for (const ksName of Object.keys(EME_KEY_SYSTEMS)) {
    if(EME_KEY_SYSTEMS[ksName].includes(ksType)) {
      return ksName;
    }
  }
  return;
}

/**
 * Build configuration for the requestMediaKeySystemAccess EME API, based
 * on the current keySystem object.
 * @param {string} [ksName] - Generic name for the key system. e.g. "clearkey",
 * "widevine", "playready". Can be used to make exceptions depending on it.
 * @param {Object} keySystem
 * @param {Boolean} [keySystem.persistentLicense]
 * @param {Boolean} [keySystem.persistentStateRequired]
 * @param {Boolean} [keySystem.distinctiveIdentifierRequired]
 * @returns {Array.<Object>} - Configuration to give to the
 * requestMediaKeySystemAccess API.
 */
function buildKeySystemConfigurations(ksName, keySystem) {
  const sessionTypes = ["temporary"];
  let persistentState = "optional";
  let distinctiveIdentifier = "optional";

  if (keySystem.persistentLicense) {
    persistentState = "required";
    sessionTypes.push("persistent-license");
  }

  if (keySystem.persistentStateRequired) {
    persistentState = "required";
  }

  if (keySystem.distinctiveIdentifierRequired) {
    distinctiveIdentifier = "required";
  }

  // Set robustness, in order of consideration:
  //   1. the user specified its own robustnesses
  //   2. a "widevine" key system is used, in that case set the default widevine
  //      robustnesses as defined in the config
  //   3. set an undefined robustness
  const videoRobustnesses = keySystem.videoRobustnesses ||
    (ksName === "widevine" ? EME_DEFAULT_WIDEVINE_ROBUSTNESSES : []);
  const audioRobustnesses = keySystem.audioRobustnesses ||
    (ksName === "widevine" ? EME_DEFAULT_WIDEVINE_ROBUSTNESSES : []);

  if (!videoRobustnesses.length) {
    videoRobustnesses.push(undefined);
  }

  if (!audioRobustnesses.length) {
    audioRobustnesses.push(undefined);
  }

  // From the W3 EME spec, we have to provide videoCapabilities and
  // audioCapabilities.
  // These capabilities must specify a codec (even though your stream can use
  // a completely different codec afterward).
  // It is also strongly recommended to specify the required security
  // robustness. As we do not want to forbide any security level, we specify
  // every existing security level from highest to lowest so that the best
  // security level is selected.
  // More details here:
  // https://storage.googleapis.com/wvdocs/Chrome_EME_Changes_and_Best_Practices.pdf
  // https://www.w3.org/TR/encrypted-media/#get-supported-configuration-and-consent
  const videoCapabilities = videoRobustnesses.map(robustness => ({
    contentType: "video/mp4;codecs=\"avc1.4d401e\"", // standard mp4 codec
    robustness,
  }));
  const audioCapabilities = audioRobustnesses.map(robustness => ({
    contentType: "audio/mp4;codecs=\"mp4a.40.2\"", // standard mp4 codec
    robustness,
  }));

  return [{
    initDataTypes: ["cenc"],
    videoCapabilities,
    audioCapabilities,
    distinctiveIdentifier,
    persistentState,
    sessionTypes,
  }, {
    // TODO Re-test with a set contentType but an undefined robustness on the
    // STBs on which this problem was found.
    //
    // add another with no {audio,video}Capabilities for some legacy browsers.
    // As of today's spec, this should return NotSupported but the first
    // candidate configuration should be good, so we should have no downside
    // doing that.
    // initDataTypes: ["cenc"],
    // videoCapabilities: undefined,
    // audioCapabilities: undefined,
    // distinctiveIdentifier,
    // persistentState,
    // sessionTypes,
  }];
}

/**
 * Try to find a compatible key system from the keySystems array given.
 *
 * Returns an Observable which, when subscribed to, will request a
 * MediaKeySystemAccess based on the various keySystems provided. This
 * Observable will:
 *   - emit the MediaKeySystemAccess and the keySystems as an object, when
 *     found. The object is under this form:
 *     {
 *       keySystemAccess {MediaKeySystemAccess}
 *       keySystem {Object}
 *     }
 *   - complete immediately after emitting.
 *   - throw if no  compatible key system has been found.
 *
 * @param {Array.<Object>} keySystems - The keySystems you want to test.
 * @returns {Observable}
 */
function findCompatibleKeySystem(keySystems, instanceInfos) {
  // Fast way to find a compatible keySystem if the currently loaded
  // one as exactly the same compatibility options.
  const cachedKeySystemAccess =
    getCachedKeySystemAccess(keySystems, instanceInfos);
  if (cachedKeySystemAccess) {
    log.debug("eme: found compatible keySystem quickly", cachedKeySystemAccess);
    return Observable.of(cachedKeySystemAccess);
  }

  /**
   * Array of set keySystems for this content.
   * Each item of this array is an object containing the following keys:
   *   - keyName {string}: keySystem canonical name (e.g. "widevine")
   *   - keyType {string}: keySystem type (e.g. "com.widevine.alpha")
   *   - keySystem {Object}: the original keySystem object
   * @type {Array.<Object>}
   */
  const keySystemsType = keySystems.reduce(
    (arr, keySystem) => {

      let ksType;

      if (EME_KEY_SYSTEMS[keySystem.type]) {
        ksType = EME_KEY_SYSTEMS[keySystem.type].map((keyType) => {
          const keyName = keySystem.type;
          return { keyName, keyType, keySystem };
        }
        );
      }
      else {
        const keyName = findKeySystemCanonicalName(keySystem.type);
        const keyType = keySystem.type;
        ksType = [{ keyName, keyType, keySystem }];
      }

      return arr.concat(ksType);
    }
    , []);

  return Observable.create((obs) => {
    let disposed = false;
    let sub = null;

    /**
     * Test the key system as defined in keySystemsType[index].
     * @param {Number} index
     */
    function testKeySystem(index) {
      // completely quit the loop if unsubscribed
      if (disposed) {
        return;
      }

      // if we iterated over the whole keySystemsType Array, quit on error
      if (index >= keySystemsType.length) {
        obs.error(new EncryptedMediaError("INCOMPATIBLE_KEYSYSTEMS", null, true));
        return;
      }

      const { keyName, keyType, keySystem } = keySystemsType[index];
      const keySystemConfigurations =
        buildKeySystemConfigurations(keyName, keySystem);

      log.debug(
        `eme: request keysystem access ${keyType},` +
        `${index+1} of ${keySystemsType.length}`,
        keySystemConfigurations
      );

      sub = requestMediaKeySystemAccess(keyType, keySystemConfigurations)
        .subscribe(
          (keySystemAccess) => {
            log.info("eme: found compatible keysystem", keyType, keySystemConfigurations);
            obs.next({ keySystem, keySystemAccess });
            obs.complete();
          },
          () => {
            log.debug("eme: rejected access to keysystem", keyType, keySystemConfigurations);
            sub = null;
            testKeySystem(index + 1);
          }
        );
    }

    testKeySystem(0);

    return () => {
      disposed = true;
      if (sub) {
        sub.unsubscribe();
      }
    };
  });
}

function getKeySystem(instanceInfos = {}) {
  return instanceInfos.$keySystem && instanceInfos.$keySystem.type;
}

export {
  findCompatibleKeySystem,
  getKeySystem,
};

export default findCompatibleKeySystem;
<commit_msg>eme: fix keySystem typo<commit_after>/**
 * Copyright 2015 CANAL+ Group
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { Observable } from "rxjs/Observable";

import config from "../../config.js";
import log from "../../utils/log";
import {
  KeySystemAccess,
  requestMediaKeySystemAccess,
  shouldRenewMediaKeys,
} from "../../compat";

import {
  EncryptedMediaError,
} from "../../errors";

const {
  EME_DEFAULT_WIDEVINE_ROBUSTNESSES,
  EME_KEY_SYSTEMS,
} = config;

function getCachedKeySystemAccess(keySystems, instanceInfos = {}) {
  const {
    $keySystem,
    $mediaKeys,
    $mediaKeySystemConfiguration,
  }  = instanceInfos;

  // NOTE(pierre): alwaysRenew flag is used for IE11 which require the
  // creation of a new MediaKeys instance for each session creation
  if (!$keySystem || !$mediaKeys || shouldRenewMediaKeys()) {
    return null;
  }

  const configuration = $mediaKeySystemConfiguration;
  const foundKeySystem = keySystems.filter((ks) => {
    if (ks.type !== $keySystem.type) {
      return false;
    }

    if (ks.persistentLicense &&
        configuration.persistentState != "required") {
      return false;
    }

    if (ks.distinctiveIdentifierRequired &&
        configuration.distinctiveIdentifier != "required") {
      return false;
    }

    return true;
  })[0];

  if (foundKeySystem) {
    return {
      keySystem: foundKeySystem,
      keySystemAccess: new KeySystemAccess(
        $keySystem.type,
        $mediaKeys,
        $mediaKeySystemConfiguration),
    };
  }
  else {
    return null;
  }
}

/**
* Find key system canonical name from key system type.
* @param {string} ksType - Obtained via inversion
* @returns {string} - Either the canonical name, or undefined.
*/
function findKeySystemCanonicalName(ksType) {
  for (const ksName of Object.keys(EME_KEY_SYSTEMS)) {
    if(EME_KEY_SYSTEMS[ksName].includes(ksType)) {
      return ksName;
    }
  }
  return;
}

/**
 * Build configuration for the requestMediaKeySystemAccess EME API, based
 * on the current keySystem object.
 * @param {string} [ksName] - Generic name for the key system. e.g. "clearkey",
 * "widevine", "playready". Can be used to make exceptions depending on it.
 * @param {Object} keySystem
 * @param {Boolean} [keySystem.persistentLicense]
 * @param {Boolean} [keySystem.persistentStateRequired]
 * @param {Boolean} [keySystem.distinctiveIdentifierRequired]
 * @returns {Array.<Object>} - Configuration to give to the
 * requestMediaKeySystemAccess API.
 */
function buildKeySystemConfigurations(ksName, keySystem) {
  const sessionTypes = ["temporary"];
  let persistentState = "optional";
  let distinctiveIdentifier = "optional";

  if (keySystem.persistentLicense) {
    persistentState = "required";
    sessionTypes.push("persistent-license");
  }

  if (keySystem.persistentStateRequired) {
    persistentState = "required";
  }

  if (keySystem.distinctiveIdentifierRequired) {
    distinctiveIdentifier = "required";
  }

  // Set robustness, in order of consideration:
  //   1. the user specified its own robustnesses
  //   2. a "widevine" key system is used, in that case set the default widevine
  //      robustnesses as defined in the config
  //   3. set an undefined robustness
  const videoRobustnesses = keySystem.videoRobustnesses ||
    (ksName === "widevine" ? EME_DEFAULT_WIDEVINE_ROBUSTNESSES : []);
  const audioRobustnesses = keySystem.audioRobustnesses ||
    (ksName === "widevine" ? EME_DEFAULT_WIDEVINE_ROBUSTNESSES : []);

  if (!videoRobustnesses.length) {
    videoRobustnesses.push(undefined);
  }

  if (!audioRobustnesses.length) {
    audioRobustnesses.push(undefined);
  }

  // From the W3 EME spec, we have to provide videoCapabilities and
  // audioCapabilities.
  // These capabilities must specify a codec (even though your stream can use
  // a completely different codec afterward).
  // It is also strongly recommended to specify the required security
  // robustness. As we do not want to forbide any security level, we specify
  // every existing security level from highest to lowest so that the best
  // security level is selected.
  // More details here:
  // https://storage.googleapis.com/wvdocs/Chrome_EME_Changes_and_Best_Practices.pdf
  // https://www.w3.org/TR/encrypted-media/#get-supported-configuration-and-consent
  const videoCapabilities = videoRobustnesses.map(robustness => ({
    contentType: "video/mp4;codecs=\"avc1.4d401e\"", // standard mp4 codec
    robustness,
  }));
  const audioCapabilities = audioRobustnesses.map(robustness => ({
    contentType: "audio/mp4;codecs=\"mp4a.40.2\"", // standard mp4 codec
    robustness,
  }));

  return [{
    initDataTypes: ["cenc"],
    videoCapabilities,
    audioCapabilities,
    distinctiveIdentifier,
    persistentState,
    sessionTypes,
  }, {
    // TODO Re-test with a set contentType but an undefined robustness on the
    // STBs on which this problem was found.
    //
    // add another with no {audio,video}Capabilities for some legacy browsers.
    // As of today's spec, this should return NotSupported but the first
    // candidate configuration should be good, so we should have no downside
    // doing that.
    // initDataTypes: ["cenc"],
    // videoCapabilities: undefined,
    // audioCapabilities: undefined,
    // distinctiveIdentifier,
    // persistentState,
    // sessionTypes,
  }];
}

/**
 * Try to find a compatible key system from the keySystems array given.
 *
 * Returns an Observable which, when subscribed to, will request a
 * MediaKeySystemAccess based on the various keySystems provided. This
 * Observable will:
 *   - emit the MediaKeySystemAccess and the keySystems as an object, when
 *     found. The object is under this form:
 *     {
 *       keySystemAccess {MediaKeySystemAccess}
 *       keySystem {Object}
 *     }
 *   - complete immediately after emitting.
 *   - throw if no  compatible key system has been found.
 *
 * @param {Array.<Object>} keySystems - The keySystems you want to test.
 * @returns {Observable}
 */
function findCompatibleKeySystem(keySystems, instanceInfos) {
  // Fast way to find a compatible keySystem if the currently loaded
  // one as exactly the same compatibility options.
  const cachedKeySystemAccess =
    getCachedKeySystemAccess(keySystems, instanceInfos);
  if (cachedKeySystemAccess) {
    log.debug("eme: found compatible keySystem quickly", cachedKeySystemAccess);
    return Observable.of(cachedKeySystemAccess);
  }

  /**
   * Array of set keySystems for this content.
   * Each item of this array is an object containing the following keys:
   *   - keyName {string}: keySystem canonical name (e.g. "widevine")
   *   - keyType {string}: keySystem type (e.g. "com.widevine.alpha")
   *   - keySystem {Object}: the original keySystem object
   * @type {Array.<Object>}
   */
  const keySystemsType = keySystems.reduce(
    (arr, keySystem) => {

      let ksType;

      if (EME_KEY_SYSTEMS[keySystem.type]) {
        ksType = EME_KEY_SYSTEMS[keySystem.type].map((keyType) => {
          const keyName = keySystem.type;
          return { keyName, keyType, keySystem };
        }
        );
      }
      else {
        const keyName = findKeySystemCanonicalName(keySystem.type);
        const keyType = keySystem.type;
        ksType = [{ keyName, keyType, keySystem }];
      }

      return arr.concat(ksType);
    }
    , []);

  return Observable.create((obs) => {
    let disposed = false;
    let sub = null;

    /**
     * Test the key system as defined in keySystemsType[index].
     * @param {Number} index
     */
    function testKeySystem(index) {
      // completely quit the loop if unsubscribed
      if (disposed) {
        return;
      }

      // if we iterated over the whole keySystemsType Array, quit on error
      if (index >= keySystemsType.length) {
        obs.error(new EncryptedMediaError("INCOMPATIBLE_KEYSYSTEMS", null, true));
        return;
      }

      const { keyName, keyType, keySystem } = keySystemsType[index];
      const keySystemConfigurations =
        buildKeySystemConfigurations(keyName, keySystem);

      log.debug(
        `eme: request keysystem access ${keyType},` +
        `${index+1} of ${keySystemsType.length}`,
        keySystemConfigurations
      );

      sub = requestMediaKeySystemAccess(keyType, keySystemConfigurations)
        .subscribe(
          (keySystemAccess) => {
            log.info("eme: found compatible keysystem", keyType, keySystemConfigurations);
            obs.next({ keySystem, keySystemAccess });
            obs.complete();
          },
          () => {
            log.debug("eme: rejected access to keysystem", keyType, keySystemConfigurations);
            sub = null;
            testKeySystem(index + 1);
          }
        );
    }

    testKeySystem(0);

    return () => {
      disposed = true;
      if (sub) {
        sub.unsubscribe();
      }
    };
  });
}

function getKeySystem(instanceInfos = {}) {
  return instanceInfos.$keySystem && instanceInfos.$keySystem.type;
}

export {
  findCompatibleKeySystem,
  getKeySystem,
};

export default findCompatibleKeySystem;
<|endoftext|>


<commit_before>import EventEmitter from 'events';
import Log from '../utils/logger.js';
import MSEEvents from './mse-events.js';
import {SampleInfo, IDRSampleList} from './media-segment-info.js';
import {IllegalStateException} from '../utils/exception.js';

// Media Source Extensions controller
class MSEController {

    constructor() {
        this.TAG = this.constructor.name;

        this._emitter = new EventEmitter();

        this.e = {
            onSourceOpen: this._onSourceOpen.bind(this),
            onSourceEnded: this._onSourceEnded.bind(this),
            onSourceClose: this._onSourceClose.bind(this),
            onSourceBufferError: this._onSourceBufferError.bind(this),
            onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)
        };

        this._mediaSource = null;
        this._mediaSourceObjectURL = null;
        this._mediaElement = null;

        this._isBufferFull = false;
        this._hasPendingEos = false;

        this._pendingSourceBufferInit = [];
        this._mimeTypes = {
            video: null,
            audio: null
        };
        this._sourceBuffers = {
            video: null,
            audio: null
        };
        this._pendingSegments = {
            video: [],
            audio: []
        };
        this._pendingRemoveRanges = {
            video: [],
            audio: []
        };
        this._idrList = new IDRSampleList();
    }

    destroy() {
        if (this._mediaElement || this._mediaSource) {
            this.detachMediaElement();
        }
        this.e = null;
        this._emitter.removeAllListeners();
        this._emitter = null;
    }

    on(event, listener) {
        this._emitter.addListener(event, listener);
    }

    off(event, listener) {
        this._emitter.removeListener(event, listener);
    }

    attachMediaElement(mediaElement) {
        if (this._mediaSource) {
            throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
        }
        let ms = this._mediaSource = new window.MediaSource();
        ms.addEventListener('sourceopen', this.e.onSourceOpen);
        ms.addEventListener('sourceended', this.e.onSourceEnded);
        ms.addEventListener('sourceclose', this.e.onSourceClose);

        this._mediaElement = mediaElement;
        this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource);
        mediaElement.src = this._mediaSourceObjectURL;
    }

    detachMediaElement() {
        if (this._mediaSource) {
            let ms = this._mediaSource;
            for (let type in this._sourceBuffers) {
                // pending segments should be discard
                let ps = this._pendingSegments[type];
                ps.splice(0, ps.length);
                this._pendingSegments[type] = null;
                this._pendingRemoveRanges[type] = null;

                // remove all sourcebuffers
                let sb = this._sourceBuffers[type];
                if (sb) {
                    ms.removeSourceBuffer(sb);
                    sb.removeEventListener('error', this.e.onSourceBufferError);
                    sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd);
                    this._mimeTypes[type] = null;
                    this._sourceBuffers[type] = null;
                }
            }
            if (ms.readyState === 'open') {
                try {
                    ms.endOfStream();
                } catch (error) {
                    Log.e(this.TAG, error.message);
                }
            }
            ms.removeEventListener('sourceopen', this.e.onSourceOpen);
            ms.removeEventListener('sourceended', this.e.onSourceEnded);
            ms.removeEventListener('sourceclose', this.e.onSourceClose);
            this._pendingSourceBufferInit = [];
            this._isBufferFull = false;
            this._idrList.clear();
            this._mediaSource = null;
        }

        if (this._mediaElement) {
            this._mediaElement.src = '';
            this._mediaElement.removeAttribute('src');
            this._mediaElement = null;
        }
        if (this._mediaSourceObjectURL) {
            window.URL.revokeObjectURL(this._mediaSourceObjectURL);
            this._mediaSourceObjectURL = null;
        }
    }

    appendInitSegment(initSegment, deferred) {
        if (!this._mediaSource || this._mediaSource.readyState !== 'open') {
            // sourcebuffer creation requires mediaSource.readyState === 'open'
            // so we defer the sourcebuffer creation, until sourceopen event triggered
            this._pendingSourceBufferInit.push(initSegment);
            // make sure that this InitSegment is in the front of pending segments queue
            this._pendingSegments[initSegment.type].push(initSegment);
            return;
        }

        let is = initSegment;
        let mimeType = `${is.container};codecs=${is.codec}`;
        let firstInitSegment = false;

        Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);
        if (mimeType !== this._mimeTypes[is.type]) {
            if (!this._mimeTypes[is.type]) {  // empty, first chance create sourcebuffer
                firstInitSegment = true;
                try {
                    let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
                    sb.addEventListener('error', this.e.onSourceBufferError);
                    sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
                } catch (error) {
                    Log.e(this.TAG, error.message);
                    this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
                    return;
                }
            } else {
                Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);
            }
            this._mimeTypes[is.type] = mimeType;
        }

        if (!deferred) {
            // deferred means this InitSegment has been pushed to pendingSegments queue
            this._pendingSegments[is.type].push(is);
        }
        if (!firstInitSegment) {  // append immediately only if init segment in subsequence
            if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {
                this._doAppendSegments();
            }
        }
    }

    appendMediaSegment(mediaSegment) {
        let ms = mediaSegment;
        this._pendingSegments[ms.type].push(ms);

        let sb = this._sourceBuffers[ms.type];
        if (sb && !sb.updating) {
            this._doAppendSegments();
        }
    }

    seek(seconds) {
        // remove all appended buffers
        for (let type in this._sourceBuffers) {
            if (!this._sourceBuffers[type]) {
                continue;
            }

            // abort current buffer append algorithm
            let sb = this._sourceBuffers[type];
            if (this._mediaSource.readyState === 'open') {
                try {
                    // If range removal algorithm is running, InvalidStateError will be throwed
                    // Ignore it.
                    sb.abort();
                } catch (error) {
                    Log.e(this.TAG, error.message);
                }
            }

            // IDRList should be clear
            this._idrList.clear();

            // record ranges to be remove from SourceBuffer
            for (let i = 0; i < sb.buffered.length; i++) {
                let start = sb.buffered.start(i);
                let end = sb.buffered.end(i);
                this._pendingRemoveRanges[type].push({start, end});
            }

            // pending segments should be discard
            let ps = this._pendingSegments[type];
            ps.splice(0, ps.length);

            // if sb is not updating, let's remove ranges now!
            if (!sb.updating) {
                this._doRemoveRanges();
            }
        }
    }

    endOfStream() {
        let ms = this._mediaSource;
        let sb = this._sourceBuffers;
        if (!ms || ms.readyState !== 'open') {
            return;
        }
        if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {
            // If any sourcebuffer is updating, defer endOfStream operation
            // See _onSourceBufferUpdateEnd()
            this._hasPendingEos = true;
        } else {
            this._hasPendingEos = false;
            // Notify media data loading complete
            // This is helpful for correcting total duration to match last media segment
            // Otherwise MediaElement's ended event may not be triggered
            ms.endOfStream();
        }
    }

    getNearestKeyframe(dts) {
        return this._idrList.getLastSyncPointBeforeDts(dts);
    }

    _doRemoveRanges() {
        for (let type in this._pendingRemoveRanges) {
            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
                continue;
            }
            let sb = this._sourceBuffers[type];
            let ranges = this._pendingRemoveRanges[type];
            while (ranges.length && !sb.updating) {
                let range = ranges.shift();
                sb.remove(range.start, range.end);
            }
        }
    }

    _doAppendSegments() {
        let pendingSegments = this._pendingSegments;

        for (let type in pendingSegments) {
            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
                continue;
            }
            if (pendingSegments[type].length > 0) {
                let segment = pendingSegments[type].shift();
                try {
                    this._sourceBuffers[type].appendBuffer(segment.data);
                    this._isBufferFull = false;
                    if (type === 'video' && segment.hasOwnProperty('info')) {
                        this._idrList.appendArray(segment.info.syncPoints);
                    }
                } catch (error) {
                    this._pendingSegments[type].unshift(segment);
                    if (error.code === 22) {  // QuotaExceededError
                        /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full
                         * Currently we can only do lazy-load to avoid SourceBuffer become scattered.
                         * SourceBuffer eviction policy may be changed in future version of FireFox.
                         *
                         * Related issues:
                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885
                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023
                         */

                        // report buffer full, abort network IO
                        if (!this._isBufferFull) {
                            this._emitter.emit(MSEEvents.BUFFER_FULL);
                        }
                        this._isBufferFull = true;
                    } else {
                        Log.e(this.TAG, error.message);
                        this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
                    }
                }
            }
        }
    }

    _onSourceOpen() {
        this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
        // deferred sourcebuffer creation / initialization
        if (this._pendingSourceBufferInit.length > 0) {
            let pendings = this._pendingSourceBufferInit;
            while (pendings.length) {
                let segment = pendings.shift();
                this.appendInitSegment(segment, true);
            }
        }
    }

    _onSourceEnded() {
        // fired on endOfStream
    }

    _onSourceClose() {
        // fired on detaching from media element
        if (this._mediaSource && this.e != null) {
            this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
            this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);
            this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);
        }
    }

    _hasPendingSegments() {
        let ps = this._pendingSegments;
        return ps.video.length > 0 || ps.audio.length > 0;
    }

    _hasPendingRemoveRanges() {
        let prr = this._pendingRemoveRanges;
        return prr.video.length > 0 || prr.audio.length > 0;
    }

    _onSourceBufferUpdateEnd() {
        if (this._hasPendingRemoveRanges()) {
            this._doRemoveRanges();
        } else if (this._hasPendingSegments()) {
            this._doAppendSegments();
        } else if (this._hasPendingEos) {
            this.endOfStream();
        }
        this._emitter.emit(MSEEvents.UPDATE_END);
    }

    _onSourceBufferError(e) {
        Log.e(this.TAG, `SourceBuffer Error: ${e}`);
        // this error might not always be fatal, just ignore it
    }

}

export default MSEController;<commit_msg>MSEController: Fix non-buffered bug under background tab<commit_after>import EventEmitter from 'events';
import Log from '../utils/logger.js';
import MSEEvents from './mse-events.js';
import {SampleInfo, IDRSampleList} from './media-segment-info.js';
import {IllegalStateException} from '../utils/exception.js';

// Media Source Extensions controller
class MSEController {

    constructor() {
        this.TAG = this.constructor.name;

        this._emitter = new EventEmitter();

        this.e = {
            onSourceOpen: this._onSourceOpen.bind(this),
            onSourceEnded: this._onSourceEnded.bind(this),
            onSourceClose: this._onSourceClose.bind(this),
            onSourceBufferError: this._onSourceBufferError.bind(this),
            onSourceBufferUpdateEnd: this._onSourceBufferUpdateEnd.bind(this)
        };

        this._mediaSource = null;
        this._mediaSourceObjectURL = null;
        this._mediaElement = null;

        this._isBufferFull = false;
        this._hasPendingEos = false;

        this._pendingSourceBufferInit = [];
        this._mimeTypes = {
            video: null,
            audio: null
        };
        this._sourceBuffers = {
            video: null,
            audio: null
        };
        this._pendingSegments = {
            video: [],
            audio: []
        };
        this._pendingRemoveRanges = {
            video: [],
            audio: []
        };
        this._idrList = new IDRSampleList();
    }

    destroy() {
        if (this._mediaElement || this._mediaSource) {
            this.detachMediaElement();
        }
        this.e = null;
        this._emitter.removeAllListeners();
        this._emitter = null;
    }

    on(event, listener) {
        this._emitter.addListener(event, listener);
    }

    off(event, listener) {
        this._emitter.removeListener(event, listener);
    }

    attachMediaElement(mediaElement) {
        if (this._mediaSource) {
            throw new IllegalStateException('MediaSource has been attached to an HTMLMediaElement!');
        }
        let ms = this._mediaSource = new window.MediaSource();
        ms.addEventListener('sourceopen', this.e.onSourceOpen);
        ms.addEventListener('sourceended', this.e.onSourceEnded);
        ms.addEventListener('sourceclose', this.e.onSourceClose);

        this._mediaElement = mediaElement;
        this._mediaSourceObjectURL = window.URL.createObjectURL(this._mediaSource);
        mediaElement.src = this._mediaSourceObjectURL;
    }

    detachMediaElement() {
        if (this._mediaSource) {
            let ms = this._mediaSource;
            for (let type in this._sourceBuffers) {
                // pending segments should be discard
                let ps = this._pendingSegments[type];
                ps.splice(0, ps.length);
                this._pendingSegments[type] = null;
                this._pendingRemoveRanges[type] = null;

                // remove all sourcebuffers
                let sb = this._sourceBuffers[type];
                if (sb) {
                    ms.removeSourceBuffer(sb);
                    sb.removeEventListener('error', this.e.onSourceBufferError);
                    sb.removeEventListener('updateend', this.e.onSourceBufferUpdateEnd);
                    this._mimeTypes[type] = null;
                    this._sourceBuffers[type] = null;
                }
            }
            if (ms.readyState === 'open') {
                try {
                    ms.endOfStream();
                } catch (error) {
                    Log.e(this.TAG, error.message);
                }
            }
            ms.removeEventListener('sourceopen', this.e.onSourceOpen);
            ms.removeEventListener('sourceended', this.e.onSourceEnded);
            ms.removeEventListener('sourceclose', this.e.onSourceClose);
            this._pendingSourceBufferInit = [];
            this._isBufferFull = false;
            this._idrList.clear();
            this._mediaSource = null;
        }

        if (this._mediaElement) {
            this._mediaElement.src = '';
            this._mediaElement.removeAttribute('src');
            this._mediaElement = null;
        }
        if (this._mediaSourceObjectURL) {
            window.URL.revokeObjectURL(this._mediaSourceObjectURL);
            this._mediaSourceObjectURL = null;
        }
    }

    appendInitSegment(initSegment, deferred) {
        if (!this._mediaSource || this._mediaSource.readyState !== 'open') {
            // sourcebuffer creation requires mediaSource.readyState === 'open'
            // so we defer the sourcebuffer creation, until sourceopen event triggered
            this._pendingSourceBufferInit.push(initSegment);
            // make sure that this InitSegment is in the front of pending segments queue
            this._pendingSegments[initSegment.type].push(initSegment);
            return;
        }

        let is = initSegment;
        let mimeType = `${is.container};codecs=${is.codec}`;
        let firstInitSegment = false;

        Log.v(this.TAG, 'Received Initialization Segment, mimeType: ' + mimeType);
        if (mimeType !== this._mimeTypes[is.type]) {
            if (!this._mimeTypes[is.type]) {  // empty, first chance create sourcebuffer
                firstInitSegment = true;
                try {
                    let sb = this._sourceBuffers[is.type] = this._mediaSource.addSourceBuffer(mimeType);
                    sb.addEventListener('error', this.e.onSourceBufferError);
                    sb.addEventListener('updateend', this.e.onSourceBufferUpdateEnd);
                } catch (error) {
                    Log.e(this.TAG, error.message);
                    this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
                    return;
                }
            } else {
                Log.v(this.TAG, `Notice: ${is.type} mimeType changed, origin: ${this._mimeTypes[is.type]}, target: ${mimeType}`);
            }
            this._mimeTypes[is.type] = mimeType;
        }

        if (!deferred) {
            // deferred means this InitSegment has been pushed to pendingSegments queue
            this._pendingSegments[is.type].push(is);
        }
        if (!firstInitSegment) {  // append immediately only if init segment in subsequence
            if (this._sourceBuffers[is.type] && !this._sourceBuffers[is.type].updating) {
                this._doAppendSegments();
            }
        }
    }

    appendMediaSegment(mediaSegment) {
        let ms = mediaSegment;
        this._pendingSegments[ms.type].push(ms);

        let sb = this._sourceBuffers[ms.type];
        if (sb && !sb.updating) {
            this._doAppendSegments();
        }
    }

    seek(seconds) {
        // remove all appended buffers
        for (let type in this._sourceBuffers) {
            if (!this._sourceBuffers[type]) {
                continue;
            }

            // abort current buffer append algorithm
            let sb = this._sourceBuffers[type];
            if (this._mediaSource.readyState === 'open') {
                try {
                    // If range removal algorithm is running, InvalidStateError will be throwed
                    // Ignore it.
                    sb.abort();
                } catch (error) {
                    Log.e(this.TAG, error.message);
                }
            }

            // IDRList should be clear
            this._idrList.clear();

            // record ranges to be remove from SourceBuffer
            for (let i = 0; i < sb.buffered.length; i++) {
                let start = sb.buffered.start(i);
                let end = sb.buffered.end(i);
                this._pendingRemoveRanges[type].push({start, end});
            }

            // pending segments should be discard
            let ps = this._pendingSegments[type];
            ps.splice(0, ps.length);

            // if sb is not updating, let's remove ranges now!
            if (!sb.updating) {
                this._doRemoveRanges();
            }
        }
    }

    endOfStream() {
        let ms = this._mediaSource;
        let sb = this._sourceBuffers;
        if (!ms || ms.readyState !== 'open') {
            return;
        }
        if (sb.video && sb.video.updating || sb.audio && sb.audio.updating) {
            // If any sourcebuffer is updating, defer endOfStream operation
            // See _onSourceBufferUpdateEnd()
            this._hasPendingEos = true;
        } else {
            this._hasPendingEos = false;
            // Notify media data loading complete
            // This is helpful for correcting total duration to match last media segment
            // Otherwise MediaElement's ended event may not be triggered
            ms.endOfStream();
        }
    }

    getNearestKeyframe(dts) {
        return this._idrList.getLastSyncPointBeforeDts(dts);
    }

    _doRemoveRanges() {
        for (let type in this._pendingRemoveRanges) {
            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
                continue;
            }
            let sb = this._sourceBuffers[type];
            let ranges = this._pendingRemoveRanges[type];
            while (ranges.length && !sb.updating) {
                let range = ranges.shift();
                sb.remove(range.start, range.end);
            }
        }
    }

    _doAppendSegments() {
        let pendingSegments = this._pendingSegments;

        for (let type in pendingSegments) {
            if (!this._sourceBuffers[type] || this._sourceBuffers[type].updating) {
                continue;
            }
            if (pendingSegments[type].length > 0) {
                let segment = pendingSegments[type].shift();
                try {
                    this._sourceBuffers[type].appendBuffer(segment.data);
                    this._isBufferFull = false;
                    if (type === 'video' && segment.hasOwnProperty('info')) {
                        this._idrList.appendArray(segment.info.syncPoints);
                    }
                } catch (error) {
                    this._pendingSegments[type].unshift(segment);
                    if (error.code === 22) {  // QuotaExceededError
                        /* Notice that FireFox may not throw QuotaExceededError if SourceBuffer is full
                         * Currently we can only do lazy-load to avoid SourceBuffer become scattered.
                         * SourceBuffer eviction policy may be changed in future version of FireFox.
                         *
                         * Related issues:
                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1279885
                         * https://bugzilla.mozilla.org/show_bug.cgi?id=1280023
                         */

                        // report buffer full, abort network IO
                        if (!this._isBufferFull) {
                            this._emitter.emit(MSEEvents.BUFFER_FULL);
                        }
                        this._isBufferFull = true;
                    } else {
                        Log.e(this.TAG, error.message);
                        this._emitter.emit(MSEEvents.ERROR, {code: error.code, msg: error.message});
                    }
                }
            }
        }
    }

    _onSourceOpen() {
        Log.v(this.TAG, 'MediaSource onSourceOpen');
        this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
        // deferred sourcebuffer creation / initialization
        if (this._pendingSourceBufferInit.length > 0) {
            let pendings = this._pendingSourceBufferInit;
            while (pendings.length) {
                let segment = pendings.shift();
                this.appendInitSegment(segment, true);
            }
        }
        // there may be some pending media segments, append them
        if (this._hasPendingSegments()) {
            this._doAppendSegments();
        }
    }

    _onSourceEnded() {
        // fired on endOfStream
        Log.v(this.TAG, 'MediaSource onSourceEnded');
    }

    _onSourceClose() {
        // fired on detaching from media element
        Log.v(this.TAG, 'MediaSource onSourceClose');
        if (this._mediaSource && this.e != null) {
            this._mediaSource.removeEventListener('sourceopen', this.e.onSourceOpen);
            this._mediaSource.removeEventListener('sourceended', this.e.onSourceEnded);
            this._mediaSource.removeEventListener('sourceclose', this.e.onSourceClose);
        }
    }

    _hasPendingSegments() {
        let ps = this._pendingSegments;
        return ps.video.length > 0 || ps.audio.length > 0;
    }

    _hasPendingRemoveRanges() {
        let prr = this._pendingRemoveRanges;
        return prr.video.length > 0 || prr.audio.length > 0;
    }

    _onSourceBufferUpdateEnd() {
        if (this._hasPendingRemoveRanges()) {
            this._doRemoveRanges();
        } else if (this._hasPendingSegments()) {
            this._doAppendSegments();
        } else if (this._hasPendingEos) {
            this.endOfStream();
        }
        this._emitter.emit(MSEEvents.UPDATE_END);
    }

    _onSourceBufferError(e) {
        Log.e(this.TAG, `SourceBuffer Error: ${e}`);
        // this error might not always be fatal, just ignore it
    }

}

export default MSEController;<|endoftext|>


<commit_before>const i = require('../../lib/interpreter.js');

describe('simple progs', () => {
});

<commit_msg>now we can use recursion :)<commit_after>const i = require('../../lib/interpreter.js');

describe('simple progs', () => {
  it('should be able to eval functions with recursion', () => {
    const result = i.exec(`
                          (define (test n)
                            (if (< n 10)
                              (do (set! n (+ n 1))
                              (test n))))
                          `);
  });

  it('should be should return a result from function with recursion', () => {
    const result = i.exec(`
                          (define (test n)
                            (if (< n 10)
                              (do (set! n (+ n 1))
                                (test n))
                               n))
                          (test 0)
                          `);
    expect(result).toBe(10);
  });
});

<|endoftext|>


<commit_before>var test = require('tape').test,
    glob = require('glob'),
    fs = require('fs'),
    request = require('request'),
    versionCurrent = require('../package.json').version.split('.'),
    Ajv = require('ajv'),
    schema = require('../schema/source_schema.json');

var ajv = new Ajv( { loadSchema: loadSchema } );

// the schema contains a remote schema for geojson so it must be loaded async
ajv.compileAsync(schema, function (err, validate) {
    if (err) return;
    testAllSources(validate);
});

// this function instructs Ajv on how to load remote sources
function loadSchema(uri, callback) {
    request.get({url:uri}, function(err, res, body) {
        if (err || res.statusCode >= 400) {
            callback(err || new Error('Loading error: ' + res.statusCode));
        } else {
            callback(null, JSON.parse(body));
        }
    });
}

function testAllSources(validate) {
  //Ensure tests on branch are current with master
  request.get('https://raw.githubusercontent.com/openaddresses/openaddresses/master/package.json', function(err, res, masterPackage) {
      var versionMaster = JSON.parse(masterPackage).version.split('.');

      for (var i = 0; i < 3; i++) {
          if (versionMaster[i] > versionCurrent[i]) {
              console.log("Branch outdated! - Please pull new changes from openaddresses/openaddresses:master");
              process.exit(1);
          }
      }

      test('schema-validate sources', function(t) {
        // find all the sources
        var manifest = glob.sync('sources/**/*.json');

        manifest.forEach(function(source) {
          var data = JSON.parse(fs.readFileSync(source, 'utf8'));

          var valid = validate(data);
          if (!valid) {
            console.error(JSON.stringify(validate.errors, null, 2));
          }

          t.notOk(validate.errors, source);

        });

        t.end();

      })

  });
}
<commit_msg>removed debug<commit_after>var test = require('tape').test,
    glob = require('glob'),
    fs = require('fs'),
    request = require('request'),
    versionCurrent = require('../package.json').version.split('.'),
    Ajv = require('ajv'),
    schema = require('../schema/source_schema.json');

var ajv = new Ajv( { loadSchema: loadSchema } );

// the schema contains a remote schema for geojson so it must be loaded async
ajv.compileAsync(schema, function (err, validate) {
    if (err) return;
    testAllSources(validate);
});

// this function instructs Ajv on how to load remote sources
function loadSchema(uri, callback) {
    request.get({url:uri}, function(err, res, body) {
        if (err || res.statusCode >= 400) {
            callback(err || new Error('Loading error: ' + res.statusCode));
        } else {
            callback(null, JSON.parse(body));
        }
    });
}

function testAllSources(validate) {
  //Ensure tests on branch are current with master
  request.get('https://raw.githubusercontent.com/openaddresses/openaddresses/master/package.json', function(err, res, masterPackage) {
      var versionMaster = JSON.parse(masterPackage).version.split('.');

      for (var i = 0; i < 3; i++) {
          if (versionMaster[i] > versionCurrent[i]) {
              console.log("Branch outdated! - Please pull new changes from openaddresses/openaddresses:master");
              process.exit(1);
          }
      }

      test('schema-validate sources', function(t) {
        // find all the sources
        var manifest = glob.sync('sources/**/*.json');

        manifest.forEach(function(source) {
          var data = JSON.parse(fs.readFileSync(source, 'utf8'));

          var valid = validate(data);

          t.notOk(validate.errors, source);

        });

        t.end();

      })

  });
}
<|endoftext|>


<commit_before>defaultConf = {
    height: 100,
    width: 300,
    fingerprintLength: 20
};

describe("The constructor is supposed a proper Captcha object", function() {
    it('Constructor Captcha exists', function(){
        expect(Captcha).toBeDefined();
    });
    var captcha = new Captcha();
    it("Captcha object is not null", function(){
       expect(captcha).not.toBeNull();
    });
    it('captcha object should be an instance of Captcha class', function(){
        expect(captcha instanceof Captcha).toBeTruthy();
    });
    it('the height of the svg should be set to the configured height', function(){
        expect(Number(captcha.height)).toEqual(defaultConf.height);
    });
    it('the width of the svg should be set to the configured width', function(){
        expect(Number(captcha.width)).toEqual(defaultConf.width);
    });
});
<commit_msg>Add one more test and it fails<commit_after>defaultConf = {
    height: 100,
    width: 300,
    fingerprintLength: 20
};

describe("The constructor is supposed a proper Captcha object", function() {
    it('Constructor Captcha exists', function(){
        expect(Captcha).toBeDefined();
    });
    var captcha = new Captcha();
    it("Captcha object is not null", function(){
       expect(captcha).not.toBeNull();
    });
    it('captcha object should be an instance of Captcha class', function(){
        expect(captcha instanceof Captcha).toBeTruthy();
    });
    it('the height of the svg should be set to the configured height', function(){
        expect(Number(captcha.height)).toEqual(defaultConf.height);
    });
    it('the width of the svg should be set to the configured width', function(){
        expect(Number(captcha.width)).toEqual(defaultConf.width);
    });
    it('the length of the fingerprint should be set to the configured fingerprintLength', function(){
        expect(Number(captcha.fingerprintLength)).toEqual(defaultConf.fingerprintLength);
    });
});
<|endoftext|>


<commit_before>import FarceActionTypes from 'farce/ActionTypes';
import { applyMiddleware } from 'redux';

import ActionTypes from './ActionTypes';

function createMatchMiddleware(matcher) {
  return function matchMiddleware() {
    return (next) => (action) => {
      const { type, payload } = action;
      if (type !== FarceActionTypes.UPDATE_LOCATION) {
        return next(action);
      }

      return next({
        type: ActionTypes.UPDATE_MATCH,
        payload: {
          location: payload,
          ...matcher.match(payload),
        },
      });
    };
  };
}

export default function createMatchEnhancer(
  matcher,
  getFound = ({ found }) => found,
) {
  return function matchEnhancer(createStore) {
    return (...args) => {
      const middlewareEnhancer = applyMiddleware(
        createMatchMiddleware(matcher),
      );

      const store = middlewareEnhancer(createStore)(...args);

      function replaceRouteConfig(routeConfig) {
        matcher.replaceRouteConfig(routeConfig);

        store.dispatch({
          type: FarceActionTypes.UPDATE_LOCATION,
          payload: getFound(store.getState()).match.location,
        });
      }

      return {
        ...store,
        found: { matcher, replaceRouteConfig },
      };
    };
  };
}
<commit_msg>feat: add terrible API for bypassing routing updates (#880)<commit_after>import FarceActionTypes from 'farce/ActionTypes';
import { applyMiddleware } from 'redux';

import ActionTypes from './ActionTypes';

function createMatchMiddleware(matcher, getFound) {
  return function matchMiddleware(store) {
    return (next) => (action) => {
      const { type, payload } = action;
      if (type !== FarceActionTypes.UPDATE_LOCATION) {
        return next(action);
      }

      let matchPayload;
      if (!payload.doNotRerunMatch) {
        matchPayload = {
          location: payload,
          ...matcher.match(payload),
        };
      } else {
        // HAX: this is terrible, but sometimes you need to update the location without updating the routing world
        // so we mutate the current match which will keep it referencially the same and pass checks but update the location
        // on it
        const { match } = getFound(store.getState());
        match.location = payload;
        matchPayload = match;
      }

      return next({
        type: ActionTypes.UPDATE_MATCH,
        payload: matchPayload,
      });
    };
  };
}

export default function createMatchEnhancer(
  matcher,
  getFound = ({ found }) => found,
) {
  return function matchEnhancer(createStore) {
    return (...args) => {
      const middlewareEnhancer = applyMiddleware(
        createMatchMiddleware(matcher, getFound),
      );

      const store = middlewareEnhancer(createStore)(...args);

      function replaceRouteConfig(routeConfig) {
        matcher.replaceRouteConfig(routeConfig);

        store.dispatch({
          type: FarceActionTypes.UPDATE_LOCATION,
          payload: getFound(store.getState()).match.location,
        });
      }

      return {
        ...store,
        found: { matcher, replaceRouteConfig },
      };
    };
  };
}
<|endoftext|>