<commit_before>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const asyncLib = require("neo-async");
const validateOptions = require("schema-utils");
const { ConcatSource, RawSource } = require("webpack-sources");
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
const ProgressPlugin = require("./ProgressPlugin");
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
const createHash = require("./util/createHash");
const { relative, dirname } = require("./util/fs");
const { absolutify } = require("./util/identifier");

const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");

/** @typedef {import("source-map").RawSourceMap} SourceMap */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Compilation")} Compilation */
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./util/Hash")} Hash */

/**
 * @typedef {object} SourceMapTask
 * @property {Source} asset
 * @property {(string | Module)[]} modules
 * @property {string} source
 * @property {string} file
 * @property {SourceMap} sourceMap
 * @property {string} cacheIdent cache identifier
 * @property {string} cacheETag cache ETag
 */

/**
 * Creating {@link SourceMapTask} for given file
 * @param {string} file current compiled file
 * @param {Source} asset the asset
 * @param {SourceMapDevToolPluginOptions} options source map options
 * @param {Compilation} compilation compilation instance
 * @param {string} cacheIdent cache identifier
 * @param {string} cacheETag cache ETag
 * @returns {SourceMapTask | undefined} created task instance or `undefined`
 */
const getTaskForFile = (
	file,
	asset,
	options,
	compilation,
	cacheIdent,
	cacheETag
) => {
	let source;
	/** @type {SourceMap} */
	let sourceMap;
	/**
	 * Check if asset can build source map
	 */
	if (asset.sourceAndMap) {
		const sourceAndMap = asset.sourceAndMap(options);
		sourceMap = /** @type {SourceMap} */ (sourceAndMap.map);
		source = sourceAndMap.source;
	} else {
		sourceMap = /** @type {SourceMap} */ (asset.map(options));
		source = asset.source();
	}
	if (!sourceMap || typeof source !== "string") return;
	const context = compilation.options.context;
	const root = compilation.compiler.root;
	const modules = sourceMap.sources.map(source => {
		if (!source.startsWith("webpack://")) return source;
		source = absolutify(context, source.slice(10), root);
		const module = compilation.findModule(source);
		return module || source;
	});

	return {
		file,
		asset,
		source,
		sourceMap,
		modules,
		cacheIdent,
		cacheETag
	};
};

class SourceMapDevToolPlugin {
	/**
	 * @param {SourceMapDevToolPluginOptions} [options] options object
	 * @throws {Error} throws error, if got more than 1 arguments
	 */
	constructor(options = {}) {
		validateOptions(schema, options || {}, {
			name: "SourceMap DevTool Plugin",
			baseDataPath: "options"
		});

		/** @type {string | false} */
		this.sourceMapFilename = options.filename;
		/** @type {string | false} */
		this.sourceMappingURLComment =
			options.append === false
				? false
				: options.append || "\n//# source" + "MappingURL=[url]";
		/** @type {string | Function} */
		this.moduleFilenameTemplate =
			options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
		/** @type {string | Function} */
		this.fallbackModuleFilenameTemplate =
			options.fallbackModuleFilenameTemplate ||
			"webpack://[namespace]/[resourcePath]?[hash]";
		/** @type {string} */
		this.namespace = options.namespace || "";
		/** @type {SourceMapDevToolPluginOptions} */
		this.options = options;
	}

	/**
	 * Apply compiler
	 * @param {Compiler} compiler compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		const outputFs = compiler.outputFileSystem;
		const sourceMapFilename = this.sourceMapFilename;
		const sourceMappingURLComment = this.sourceMappingURLComment;
		const moduleFilenameTemplate = this.moduleFilenameTemplate;
		const namespace = this.namespace;
		const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
		const requestShortener = compiler.requestShortener;
		const options = this.options;
		options.test = options.test || /\.(m?js|css)($|\?)/i;

		const matchObject = ModuleFilenameHelpers.matchObject.bind(
			undefined,
			options
		);

		compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
			new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);

			compilation.hooks.finishAssets.tapAsync(
				"SourceMapDevToolPlugin",
				(assets, callback) => {
					const chunkGraph = compilation.chunkGraph;
					/** @type {Map<string | Module, string>} */
					const moduleToSourceNameMapping = new Map();
					/**
					 * @type {Function}
					 * @returns {void}
					 */
					const reportProgress =
						ProgressPlugin.getReporter(compilation.compiler) || (() => {});

					/** @type {Map<string, Chunk>} */
					const fileToChunk = new Map();
					for (const chunk of compilation.chunks) {
						for (const file of chunk.files) {
							fileToChunk.set(file, chunk);
						}
						for (const file of chunk.auxiliaryFiles) {
							fileToChunk.set(file, chunk);
						}
					}

					/** @type {string[]} */
					const files = [];
					for (const file of Object.keys(assets)) {
						if (matchObject(file)) {
							files.push(file);
						}
					}

					reportProgress(0.0);
					/** @type {SourceMapTask[]} */
					const tasks = [];
					let fileIndex = 0;

					asyncLib.each(
						files,
						(file, callback) => {
							const asset = compilation.getAsset(file).source;
							const cacheIdent = `${compilation.compilerPath}/SourceMapDevToolPlugin/${file}`;
							const cacheETagHash = createHash("md4");
							asset.updateHash(cacheETagHash);
							const cacheETag = cacheETagHash.digest("hex").toString();

							compilation.cache.get(cacheIdent, cacheETag, (err, assets) => {
								if (err) {
									return callback(err);
								}
								/**
								 * If presented in cache, reassigns assets. Cache assets already have source maps.
								 */
								if (assets) {
									for (const cachedFile in assets) {
										if (cachedFile === file) {
											compilation.updateAsset(cachedFile, assets[cachedFile]);
										} else {
											compilation.emitAsset(cachedFile, assets[cachedFile], {
												development: true
											});
										}
										/**
										 * Add file to chunk, if not presented there
										 */
										if (cachedFile !== file) {
											const chunk = fileToChunk.get(file);
											if (chunk !== undefined)
												chunk.auxiliaryFiles.add(cachedFile);
										}
									}

									reportProgress(
										(0.5 * fileIndex++) / files.length,
										file,
										"generate SourceMap"
									);

									return callback();
								}

								/** @type {SourceMapTask | undefined} */
								const task = getTaskForFile(
									file,
									asset,
									options,
									compilation,
									cacheIdent,
									cacheETag
								);

								if (task) {
									const modules = task.modules;

									for (let idx = 0; idx < modules.length; idx++) {
										const module = modules[idx];
										if (!moduleToSourceNameMapping.get(module)) {
											moduleToSourceNameMapping.set(
												module,
												ModuleFilenameHelpers.createFilename(
													module,
													{
														moduleFilenameTemplate: moduleFilenameTemplate,
														namespace: namespace
													},
													{
														requestShortener,
														chunkGraph
													}
												)
											);
										}
									}

									tasks.push(task);
								}

								reportProgress(
									(0.5 * fileIndex++) / files.length,
									file,
									"generate SourceMap"
								);

								callback();
							});
						},
						err => {
							if (err) {
								return callback(err);
							}

							reportProgress(0.5, "resolve sources");
							/** @type {Set<string>} */
							const usedNamesSet = new Set(moduleToSourceNameMapping.values());
							/** @type {Set<string>} */
							const conflictDetectionSet = new Set();

							/**
							 * all modules in defined order (longest identifier first)
							 * @type {Array<string | Module>}
							 */
							const allModules = Array.from(
								moduleToSourceNameMapping.keys()
							).sort((a, b) => {
								const ai = typeof a === "string" ? a : a.identifier();
								const bi = typeof b === "string" ? b : b.identifier();
								return ai.length - bi.length;
							});

							// find modules with conflicting source names
							for (let idx = 0; idx < allModules.length; idx++) {
								const module = allModules[idx];
								let sourceName = moduleToSourceNameMapping.get(module);
								let hasName = conflictDetectionSet.has(sourceName);
								if (!hasName) {
									conflictDetectionSet.add(sourceName);
									continue;
								}

								// try the fallback name first
								sourceName = ModuleFilenameHelpers.createFilename(
									module,
									{
										moduleFilenameTemplate: fallbackModuleFilenameTemplate,
										namespace: namespace
									},
									{
										requestShortener,
										chunkGraph
									}
								);
								hasName = usedNamesSet.has(sourceName);
								if (!hasName) {
									moduleToSourceNameMapping.set(module, sourceName);
									usedNamesSet.add(sourceName);
									continue;
								}

								// elsewise just append stars until we have a valid name
								while (hasName) {
									sourceName += "*";
									hasName = usedNamesSet.has(sourceName);
								}
								moduleToSourceNameMapping.set(module, sourceName);
								usedNamesSet.add(sourceName);
							}

							let taskIndex = 0;

							asyncLib.each(
								tasks,
								(task, callback) => {
									const assets = Object.create(null);
									const file = task.file;
									const chunk = fileToChunk.get(file);
									const sourceMap = task.sourceMap;
									const source = task.source;
									const modules = task.modules;
									const moduleFilenames = modules.map(m =>
										moduleToSourceNameMapping.get(m)
									);
									sourceMap.sources = moduleFilenames;
									if (options.noSources) {
										sourceMap.sourcesContent = undefined;
									}
									sourceMap.sourceRoot = options.sourceRoot || "";
									sourceMap.file = file;

									/** @type {string | false} */
									let currentSourceMappingURLComment = sourceMappingURLComment;
									if (
										currentSourceMappingURLComment !== false &&
										/\.css($|\?)/i.test(file)
									) {
										currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
											/^\n\/\/(.*)$/,
											"\n/*$1*/"
										);
									}
									const sourceMapString = JSON.stringify(sourceMap);
									if (sourceMapFilename) {
										let filename = file;
										const pathParams = {
											chunk,
											filename: options.fileContext
												? relative(
														outputFs,
														`/${options.fileContext}`,
														`/${filename}`
												  )
												: filename,
											contentHash: /** @type {string} */ (createHash("md4")
												.update(sourceMapString)
												.digest("hex"))
										};
										let sourceMapFile = compilation.getPath(
											sourceMapFilename,
											pathParams
										);
										const sourceMapUrl = options.publicPath
											? options.publicPath + sourceMapFile
											: relative(
													outputFs,
													dirname(outputFs, `/${file}`),
													`/${sourceMapFile}`
											  );
										/**
										 * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
										 */
										if (currentSourceMappingURLComment !== false) {
											const asset = new ConcatSource(
												new RawSource(source),
												compilation.getPath(
													currentSourceMappingURLComment,
													Object.assign({ url: sourceMapUrl }, pathParams)
												)
											);
											assets[file] = asset;
											compilation.updateAsset(file, asset);
										}
										/**
										 * Add source map file to compilation assets and chunk files
										 */
										const asset = new RawSource(sourceMapString);
										assets[sourceMapFile] = asset;
										compilation.emitAsset(sourceMapFile, asset, {
											development: true
										});
										if (chunk !== undefined)
											chunk.auxiliaryFiles.add(sourceMapFile);
									} else {
										if (currentSourceMappingURLComment === false) {
											throw new Error(
												"SourceMapDevToolPlugin: append can't be false when no filename is provided"
											);
										}
										/**
										 * Add source map as data url to asset
										 */
										const asset = new ConcatSource(
											new RawSource(source),
											currentSourceMappingURLComment
												.replace(/\[map\]/g, () => sourceMapString)
												.replace(
													/\[url\]/g,
													() =>
														`data:application/json;charset=utf-8;base64,${Buffer.from(
															sourceMapString,
															"utf-8"
														).toString("base64")}`
												)
										);
										assets[file] = asset;
										compilation.updateAsset(file, asset);
									}

									compilation.cache.store(
										task.cacheIdent,
										task.cacheETag,
										assets,
										err => {
											reportProgress(
												0.5 + (0.5 * taskIndex++) / tasks.length,
												task.file,
												"attach SourceMap"
											);

											if (err) {
												return callback(err);
											}
											callback();
										}
									);
								},
								err => {
									reportProgress(1.0);
									callback(err);
								}
							);
						}
					);
				}
			);
		});
	}
}

module.exports = SourceMapDevToolPlugin;
<commit_msg>digest already returns a string<commit_after>/*
	MIT License http://www.opensource.org/licenses/mit-license.php
	Author Tobias Koppers @sokra
*/

"use strict";

const asyncLib = require("neo-async");
const validateOptions = require("schema-utils");
const { ConcatSource, RawSource } = require("webpack-sources");
const ModuleFilenameHelpers = require("./ModuleFilenameHelpers");
const ProgressPlugin = require("./ProgressPlugin");
const SourceMapDevToolModuleOptionsPlugin = require("./SourceMapDevToolModuleOptionsPlugin");
const createHash = require("./util/createHash");
const { relative, dirname } = require("./util/fs");
const { absolutify } = require("./util/identifier");

const schema = require("../schemas/plugins/SourceMapDevToolPlugin.json");

/** @typedef {import("source-map").RawSourceMap} SourceMap */
/** @typedef {import("webpack-sources").Source} Source */
/** @typedef {import("../declarations/plugins/SourceMapDevToolPlugin").SourceMapDevToolPluginOptions} SourceMapDevToolPluginOptions */
/** @typedef {import("./Chunk")} Chunk */
/** @typedef {import("./Compilation")} Compilation */
/** @typedef {import("./Compiler")} Compiler */
/** @typedef {import("./Module")} Module */
/** @typedef {import("./util/Hash")} Hash */

/**
 * @typedef {object} SourceMapTask
 * @property {Source} asset
 * @property {(string | Module)[]} modules
 * @property {string} source
 * @property {string} file
 * @property {SourceMap} sourceMap
 * @property {string} cacheIdent cache identifier
 * @property {string} cacheETag cache ETag
 */

/**
 * Creating {@link SourceMapTask} for given file
 * @param {string} file current compiled file
 * @param {Source} asset the asset
 * @param {SourceMapDevToolPluginOptions} options source map options
 * @param {Compilation} compilation compilation instance
 * @param {string} cacheIdent cache identifier
 * @param {string} cacheETag cache ETag
 * @returns {SourceMapTask | undefined} created task instance or `undefined`
 */
const getTaskForFile = (
	file,
	asset,
	options,
	compilation,
	cacheIdent,
	cacheETag
) => {
	let source;
	/** @type {SourceMap} */
	let sourceMap;
	/**
	 * Check if asset can build source map
	 */
	if (asset.sourceAndMap) {
		const sourceAndMap = asset.sourceAndMap(options);
		sourceMap = /** @type {SourceMap} */ (sourceAndMap.map);
		source = sourceAndMap.source;
	} else {
		sourceMap = /** @type {SourceMap} */ (asset.map(options));
		source = asset.source();
	}
	if (!sourceMap || typeof source !== "string") return;
	const context = compilation.options.context;
	const root = compilation.compiler.root;
	const modules = sourceMap.sources.map(source => {
		if (!source.startsWith("webpack://")) return source;
		source = absolutify(context, source.slice(10), root);
		const module = compilation.findModule(source);
		return module || source;
	});

	return {
		file,
		asset,
		source,
		sourceMap,
		modules,
		cacheIdent,
		cacheETag
	};
};

class SourceMapDevToolPlugin {
	/**
	 * @param {SourceMapDevToolPluginOptions} [options] options object
	 * @throws {Error} throws error, if got more than 1 arguments
	 */
	constructor(options = {}) {
		validateOptions(schema, options || {}, {
			name: "SourceMap DevTool Plugin",
			baseDataPath: "options"
		});

		/** @type {string | false} */
		this.sourceMapFilename = options.filename;
		/** @type {string | false} */
		this.sourceMappingURLComment =
			options.append === false
				? false
				: options.append || "\n//# source" + "MappingURL=[url]";
		/** @type {string | Function} */
		this.moduleFilenameTemplate =
			options.moduleFilenameTemplate || "webpack://[namespace]/[resourcePath]";
		/** @type {string | Function} */
		this.fallbackModuleFilenameTemplate =
			options.fallbackModuleFilenameTemplate ||
			"webpack://[namespace]/[resourcePath]?[hash]";
		/** @type {string} */
		this.namespace = options.namespace || "";
		/** @type {SourceMapDevToolPluginOptions} */
		this.options = options;
	}

	/**
	 * Apply compiler
	 * @param {Compiler} compiler compiler instance
	 * @returns {void}
	 */
	apply(compiler) {
		const outputFs = compiler.outputFileSystem;
		const sourceMapFilename = this.sourceMapFilename;
		const sourceMappingURLComment = this.sourceMappingURLComment;
		const moduleFilenameTemplate = this.moduleFilenameTemplate;
		const namespace = this.namespace;
		const fallbackModuleFilenameTemplate = this.fallbackModuleFilenameTemplate;
		const requestShortener = compiler.requestShortener;
		const options = this.options;
		options.test = options.test || /\.(m?js|css)($|\?)/i;

		const matchObject = ModuleFilenameHelpers.matchObject.bind(
			undefined,
			options
		);

		compiler.hooks.compilation.tap("SourceMapDevToolPlugin", compilation => {
			new SourceMapDevToolModuleOptionsPlugin(options).apply(compilation);

			compilation.hooks.finishAssets.tapAsync(
				"SourceMapDevToolPlugin",
				(assets, callback) => {
					const chunkGraph = compilation.chunkGraph;
					/** @type {Map<string | Module, string>} */
					const moduleToSourceNameMapping = new Map();
					/**
					 * @type {Function}
					 * @returns {void}
					 */
					const reportProgress =
						ProgressPlugin.getReporter(compilation.compiler) || (() => {});

					/** @type {Map<string, Chunk>} */
					const fileToChunk = new Map();
					for (const chunk of compilation.chunks) {
						for (const file of chunk.files) {
							fileToChunk.set(file, chunk);
						}
						for (const file of chunk.auxiliaryFiles) {
							fileToChunk.set(file, chunk);
						}
					}

					/** @type {string[]} */
					const files = [];
					for (const file of Object.keys(assets)) {
						if (matchObject(file)) {
							files.push(file);
						}
					}

					reportProgress(0.0);
					/** @type {SourceMapTask[]} */
					const tasks = [];
					let fileIndex = 0;

					asyncLib.each(
						files,
						(file, callback) => {
							const asset = compilation.getAsset(file).source;
							const cacheIdent = `${compilation.compilerPath}/SourceMapDevToolPlugin/${file}`;
							const cacheETagHash = createHash("md4");
							asset.updateHash(cacheETagHash);
							const cacheETag = /** @type {string} */ (cacheETagHash.digest(
								"hex"
							));

							compilation.cache.get(cacheIdent, cacheETag, (err, assets) => {
								if (err) {
									return callback(err);
								}
								/**
								 * If presented in cache, reassigns assets. Cache assets already have source maps.
								 */
								if (assets) {
									for (const cachedFile in assets) {
										if (cachedFile === file) {
											compilation.updateAsset(cachedFile, assets[cachedFile]);
										} else {
											compilation.emitAsset(cachedFile, assets[cachedFile], {
												development: true
											});
										}
										/**
										 * Add file to chunk, if not presented there
										 */
										if (cachedFile !== file) {
											const chunk = fileToChunk.get(file);
											if (chunk !== undefined)
												chunk.auxiliaryFiles.add(cachedFile);
										}
									}

									reportProgress(
										(0.5 * fileIndex++) / files.length,
										file,
										"generate SourceMap"
									);

									return callback();
								}

								/** @type {SourceMapTask | undefined} */
								const task = getTaskForFile(
									file,
									asset,
									options,
									compilation,
									cacheIdent,
									cacheETag
								);

								if (task) {
									const modules = task.modules;

									for (let idx = 0; idx < modules.length; idx++) {
										const module = modules[idx];
										if (!moduleToSourceNameMapping.get(module)) {
											moduleToSourceNameMapping.set(
												module,
												ModuleFilenameHelpers.createFilename(
													module,
													{
														moduleFilenameTemplate: moduleFilenameTemplate,
														namespace: namespace
													},
													{
														requestShortener,
														chunkGraph
													}
												)
											);
										}
									}

									tasks.push(task);
								}

								reportProgress(
									(0.5 * fileIndex++) / files.length,
									file,
									"generate SourceMap"
								);

								callback();
							});
						},
						err => {
							if (err) {
								return callback(err);
							}

							reportProgress(0.5, "resolve sources");
							/** @type {Set<string>} */
							const usedNamesSet = new Set(moduleToSourceNameMapping.values());
							/** @type {Set<string>} */
							const conflictDetectionSet = new Set();

							/**
							 * all modules in defined order (longest identifier first)
							 * @type {Array<string | Module>}
							 */
							const allModules = Array.from(
								moduleToSourceNameMapping.keys()
							).sort((a, b) => {
								const ai = typeof a === "string" ? a : a.identifier();
								const bi = typeof b === "string" ? b : b.identifier();
								return ai.length - bi.length;
							});

							// find modules with conflicting source names
							for (let idx = 0; idx < allModules.length; idx++) {
								const module = allModules[idx];
								let sourceName = moduleToSourceNameMapping.get(module);
								let hasName = conflictDetectionSet.has(sourceName);
								if (!hasName) {
									conflictDetectionSet.add(sourceName);
									continue;
								}

								// try the fallback name first
								sourceName = ModuleFilenameHelpers.createFilename(
									module,
									{
										moduleFilenameTemplate: fallbackModuleFilenameTemplate,
										namespace: namespace
									},
									{
										requestShortener,
										chunkGraph
									}
								);
								hasName = usedNamesSet.has(sourceName);
								if (!hasName) {
									moduleToSourceNameMapping.set(module, sourceName);
									usedNamesSet.add(sourceName);
									continue;
								}

								// elsewise just append stars until we have a valid name
								while (hasName) {
									sourceName += "*";
									hasName = usedNamesSet.has(sourceName);
								}
								moduleToSourceNameMapping.set(module, sourceName);
								usedNamesSet.add(sourceName);
							}

							let taskIndex = 0;

							asyncLib.each(
								tasks,
								(task, callback) => {
									const assets = Object.create(null);
									const file = task.file;
									const chunk = fileToChunk.get(file);
									const sourceMap = task.sourceMap;
									const source = task.source;
									const modules = task.modules;
									const moduleFilenames = modules.map(m =>
										moduleToSourceNameMapping.get(m)
									);
									sourceMap.sources = moduleFilenames;
									if (options.noSources) {
										sourceMap.sourcesContent = undefined;
									}
									sourceMap.sourceRoot = options.sourceRoot || "";
									sourceMap.file = file;

									/** @type {string | false} */
									let currentSourceMappingURLComment = sourceMappingURLComment;
									if (
										currentSourceMappingURLComment !== false &&
										/\.css($|\?)/i.test(file)
									) {
										currentSourceMappingURLComment = currentSourceMappingURLComment.replace(
											/^\n\/\/(.*)$/,
											"\n/*$1*/"
										);
									}
									const sourceMapString = JSON.stringify(sourceMap);
									if (sourceMapFilename) {
										let filename = file;
										const pathParams = {
											chunk,
											filename: options.fileContext
												? relative(
														outputFs,
														`/${options.fileContext}`,
														`/${filename}`
												  )
												: filename,
											contentHash: /** @type {string} */ (createHash("md4")
												.update(sourceMapString)
												.digest("hex"))
										};
										let sourceMapFile = compilation.getPath(
											sourceMapFilename,
											pathParams
										);
										const sourceMapUrl = options.publicPath
											? options.publicPath + sourceMapFile
											: relative(
													outputFs,
													dirname(outputFs, `/${file}`),
													`/${sourceMapFile}`
											  );
										/**
										 * Add source map url to compilation asset, if {@link currentSourceMappingURLComment} presented
										 */
										if (currentSourceMappingURLComment !== false) {
											const asset = new ConcatSource(
												new RawSource(source),
												compilation.getPath(
													currentSourceMappingURLComment,
													Object.assign({ url: sourceMapUrl }, pathParams)
												)
											);
											assets[file] = asset;
											compilation.updateAsset(file, asset);
										}
										/**
										 * Add source map file to compilation assets and chunk files
										 */
										const asset = new RawSource(sourceMapString);
										assets[sourceMapFile] = asset;
										compilation.emitAsset(sourceMapFile, asset, {
											development: true
										});
										if (chunk !== undefined)
											chunk.auxiliaryFiles.add(sourceMapFile);
									} else {
										if (currentSourceMappingURLComment === false) {
											throw new Error(
												"SourceMapDevToolPlugin: append can't be false when no filename is provided"
											);
										}
										/**
										 * Add source map as data url to asset
										 */
										const asset = new ConcatSource(
											new RawSource(source),
											currentSourceMappingURLComment
												.replace(/\[map\]/g, () => sourceMapString)
												.replace(
													/\[url\]/g,
													() =>
														`data:application/json;charset=utf-8;base64,${Buffer.from(
															sourceMapString,
															"utf-8"
														).toString("base64")}`
												)
										);
										assets[file] = asset;
										compilation.updateAsset(file, asset);
									}

									compilation.cache.store(
										task.cacheIdent,
										task.cacheETag,
										assets,
										err => {
											reportProgress(
												0.5 + (0.5 * taskIndex++) / tasks.length,
												task.file,
												"attach SourceMap"
											);

											if (err) {
												return callback(err);
											}
											callback();
										}
									);
								},
								err => {
									reportProgress(1.0);
									callback(err);
								}
							);
						}
					);
				}
			);
		});
	}
}

module.exports = SourceMapDevToolPlugin;
<|endoftext|>


<commit_before>import classNames from 'classnames/dedupe';
import GeminiScrollbar from 'react-gemini-scrollbar';
/* eslint-disable no-unused-vars */
import React, {PropTypes} from 'react';
/* eslint-enable no-unused-vars */
/**
 * Lifecycle of a Modal:
 * initial page load -> empty ReactCSSTransitionGroup
 * interaction changes open to true -> render modal content without scrollbars
 * get height of content -> rerender modal content and cap the height
 */
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

import BindMixin from '../Mixin/BindMixin';
import DOMUtil from '../Util/DOMUtil';
import KeyDownMixin from '../Mixin/KeyDownMixin';
import Util from '../Util/Util';

class ModalContents extends Util.mixin(BindMixin, KeyDownMixin) {
  constructor() {
    super(...arguments);

    this.state = {
      height: null
    };
  }

  get methodsToBind() {
    return [
      'calculateContentHeight',
      'closeModal',
      'handleBackdropClick',
      'handleWindowResize'
    ];
  }

  get keysToBind() {
    return {
      esc: this.closeModal
    };
  }

  componentWillReceiveProps(nextProps) {
    super.componentWillReceiveProps(...arguments);

    if (this.props.open !== nextProps.open) {
      document.body.classList.toggle('no-overflow');
    }
  }

  componentDidUpdate() {
    super.componentDidUpdate(...arguments);

    // If we don't already know the height of the content, we calculate it.
    if (this.props.open && this.props.useGemini && this.state.height == null) {
      global.requestAnimationFrame(this.calculateContentHeight);
    }
  }

  componentWillUpdate(nextProps) {
    // Reset the height of the content to null when the modal is closing so
    // that the height will be recalculated next time it opens.
    if (this.props.open && !nextProps.open) {
      this.setState({height: null});
    }
  }

  componentWillMount() {
    super.componentWillMount(...arguments);

    if (this.props.open) {
      document.body.classList.add('no-overflow');
    }

    window.addEventListener('resize', this.handleWindowResize);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);

    document.body.classList.remove('no-overflow');
    window.removeEventListener('resize', this.handleWindowResize);
  }

  handleBackdropClick() {
    if (this.props.closeByBackdropClick) {
      this.closeModal();
    }
  }

  handleWindowResize() {
    let {props, state} = this;

    // Return early if the modal is closed or not using Gemini.
    if (!props.open || !props.useGemini) {
      return;
    }

    let viewportHeight = DOMUtil.getViewportHeight();

    // If the height of the viewport is getting shorter, or if it's growing
    // while the height is currently constrained, then we reset the restrained
    // height to null which will cause the height to be recalculated on the
    // next render.
    if (viewportHeight < this.lastViewportHeight
      || (viewportHeight > this.lastViewportHeight
        && state.height !== null)) {
      this.setState({height: null});
    }

    this.lastViewportHeight = viewportHeight;
  }

  calculateContentHeight() {
    let {innerContent, innerContentContainer} = this.refs;

    let innerContentHeight = innerContent.getBoundingClientRect().height;
    let innerContentContainerHeight = innerContentContainer
      .getBoundingClientRect().height;

    if (innerContentHeight > innerContentContainerHeight) {
      this.setState({height: innerContentContainerHeight});
    }
  }

  closeModal() {
    this.props.onClose();
  }

  getCloseButton() {
    let {props} = this;

    if (props.closeButton) {
      return props.closeButton;
    }

    return null;
  }

  getHeader() {
    let {props} = this;

    if (props.showHeader === false) {
      return null;
    }

    return (

        {props.header}
        {props.subHeader}

    );
  }

  getFooter() {
    let {props} = this;

    if (props.showFooter === false) {
      return null;
    }

    return (

        {props.footer}

    );
  }

  getModalContent() {
    let {props, state} = this;

    let modalContent = (

        {props.children}

    );

    // If we aren't rendering with Gemini, or we don't know the height of the
    // modal's content, then we render without Gemini.
    if (!props.useGemini || state.height == null) {
      return modalContent;
    }

    let geminiContainerStyle = {
      height: state.height
    };

    return (
      <GeminiScrollbar
        autoshow={false}
        className="container-scrollable"
        style={geminiContainerStyle}>
        {modalContent}
      </GeminiScrollbar>
    );
  }

  getModal() {
    let {props, state} = this;
    let modalStyle = null;

    if (!props.open) {
      return null;
    }

    if (state.height != null) {
      modalStyle = {flexBasis: state.height};
    }

    return (

        {this.getCloseButton()}
        {this.getHeader()}

          {this.getModalContent()}

        {this.getFooter()}

    );
  }

  getBackdrop() {
    let {props} = this;

    if (!props.open) {
      return null;
    }

    return (

    );
  }

  render() {
    let {props} = this;
    let modalContent = null;

    if (props.open) {
      modalContent = (

          {this.getBackdrop()}
          {this.getModal()}

      );
    }

    return (
      <ReactCSSTransitionGroup
        transitionAppear={props.transitionAppear}
        transitionEnter={props.transitionEnter}
        transitionLeave={props.transitionLeave}
        transitionName={props.transitionNameModal}
        transitionAppearTimeout={props.transitionAppearTimeoutModal}
        transitionEnterTimeout={props.transitionEnterTimeoutModal}
        transitionLeaveTimeout={props.transitionLeaveTimeoutModal}
        component="div">
        {modalContent}
      </ReactCSSTransitionGroup>
    );
  }
}

ModalContents.defaultProps = {
  closeByBackdropClick: true,
  footer: null,
  header: null,
  onClose: () => {},
  open: false,
  showHeader: false,
  showFooter: false,
  subHeader: null,
  transitionNameModal: 'modal',
  transitionAppearTimeoutModal: 300,
  transitionEnterTimeoutModal: 300,
  transitionLeaveTimeoutModal: 300,
  transitionAppear: true,
  transitionEnter: true,
  transitionLeave: true,
  useGemini: true,

  // Default classes.
  backdropClass: 'modal-backdrop',
  bodyClass: 'modal-body-wrapper',
  closeButtonClass: 'modal-close',
  footerClass: 'modal-footer',
  headerClass: 'modal-header',
  modalClass: 'modal modal-large',
  scrollContainerClass: 'modal-body'
};

ModalContents.propTypes = {
  children: PropTypes.node,
  // Appends whatever value is provided as a close button.
  closeButton: PropTypes.node,
  // Allow closing of modal when click happens outside modal. Defaults to true.
  closeByBackdropClick: PropTypes.bool,
  // Allow resize of modal to fit screen. Defaults to true.
  // Optional footer
  footer: PropTypes.object,
  // Optional header.
  header: PropTypes.node,
  // Optional callback function exected when modal is closed.
  onClose: PropTypes.func,
  // True if modal is open, false otherwise.
  open: PropTypes.bool,
  // Set true to show header. Defaults to false.
  showHeader: PropTypes.bool,
  // Set true to show footer. Defaults to false.
  showFooter: PropTypes.bool,
  // Optional subheader.
  subHeader: PropTypes.node,
  // Optional enter and leave transition name for modal
  transitionNameModal: PropTypes.string,
  // Transition lengths, must be non-zero
  transitionAppearTimeoutModal: PropTypes.number,
  transitionEnterTimeoutModal: PropTypes.number,
  transitionLeaveTimeoutModal: PropTypes.number,
  // Optionally disable transitions
  transitionAppear: PropTypes.bool,
  transitionEnter: PropTypes.bool,
  transitionLeave: PropTypes.bool,
  // Option to use Gemini scrollbar. Defaults to true.
  useGemini: PropTypes.bool,

  // Classes
  backdropClass: PropTypes.string,
  bodyClass: PropTypes.string,
  closeButtonClass: PropTypes.string,
  footerClass: PropTypes.string,
  headerClass: PropTypes.string,
  modalClass: PropTypes.string,
  modalWrapperClass: PropTypes.string,
  scrollContainerClass: PropTypes.string
};

module.exports = ModalContents;
<commit_msg>Allow setting classes on Gemini<commit_after>import classNames from 'classnames/dedupe';
import GeminiScrollbar from 'react-gemini-scrollbar';
/* eslint-disable no-unused-vars */
import React, {PropTypes} from 'react';
/* eslint-enable no-unused-vars */
/**
 * Lifecycle of a Modal:
 * initial page load -> empty ReactCSSTransitionGroup
 * interaction changes open to true -> render modal content without scrollbars
 * get height of content -> rerender modal content and cap the height
 */
import ReactCSSTransitionGroup from 'react-addons-css-transition-group';

import BindMixin from '../Mixin/BindMixin';
import DOMUtil from '../Util/DOMUtil';
import KeyDownMixin from '../Mixin/KeyDownMixin';
import Util from '../Util/Util';

class ModalContents extends Util.mixin(BindMixin, KeyDownMixin) {
  constructor() {
    super(...arguments);

    this.state = {
      height: null
    };
  }

  get methodsToBind() {
    return [
      'calculateContentHeight',
      'closeModal',
      'handleBackdropClick',
      'handleWindowResize'
    ];
  }

  get keysToBind() {
    return {
      esc: this.closeModal
    };
  }

  componentWillReceiveProps(nextProps) {
    super.componentWillReceiveProps(...arguments);

    if (this.props.open !== nextProps.open) {
      document.body.classList.toggle('no-overflow');
    }
  }

  componentDidUpdate() {
    super.componentDidUpdate(...arguments);

    // If we don't already know the height of the content, we calculate it.
    if (this.props.open && this.props.useGemini && this.state.height == null) {
      global.requestAnimationFrame(this.calculateContentHeight);
    }
  }

  componentWillUpdate(nextProps) {
    // Reset the height of the content to null when the modal is closing so
    // that the height will be recalculated next time it opens.
    if (this.props.open && !nextProps.open) {
      this.setState({height: null});
    }
  }

  componentWillMount() {
    super.componentWillMount(...arguments);

    if (this.props.open) {
      document.body.classList.add('no-overflow');
    }

    window.addEventListener('resize', this.handleWindowResize);
  }

  componentWillUnmount() {
    super.componentWillUnmount(...arguments);

    document.body.classList.remove('no-overflow');
    window.removeEventListener('resize', this.handleWindowResize);
  }

  handleBackdropClick() {
    if (this.props.closeByBackdropClick) {
      this.closeModal();
    }
  }

  handleWindowResize() {
    let {props, state} = this;

    // Return early if the modal is closed or not using Gemini.
    if (!props.open || !props.useGemini) {
      return;
    }

    let viewportHeight = DOMUtil.getViewportHeight();

    // If the height of the viewport is getting shorter, or if it's growing
    // while the height is currently constrained, then we reset the restrained
    // height to null which will cause the height to be recalculated on the
    // next render.
    if (viewportHeight < this.lastViewportHeight
      || (viewportHeight > this.lastViewportHeight
        && state.height !== null)) {
      this.setState({height: null});
    }

    this.lastViewportHeight = viewportHeight;
  }

  calculateContentHeight() {
    let {innerContent, innerContentContainer} = this.refs;

    let innerContentHeight = innerContent.getBoundingClientRect().height;
    let innerContentContainerHeight = innerContentContainer
      .getBoundingClientRect().height;

    if (innerContentHeight > innerContentContainerHeight) {
      this.setState({height: innerContentContainerHeight});
    }
  }

  closeModal() {
    this.props.onClose();
  }

  getCloseButton() {
    let {props} = this;

    if (props.closeButton) {
      return props.closeButton;
    }

    return null;
  }

  getHeader() {
    let {props} = this;

    if (props.showHeader === false) {
      return null;
    }

    return (

        {props.header}
        {props.subHeader}

    );
  }

  getFooter() {
    let {props} = this;

    if (props.showFooter === false) {
      return null;
    }

    return (

        {props.footer}

    );
  }

  getModalContent() {
    let {props, state} = this;

    let modalContent = (

        {props.children}

    );

    // If we aren't rendering with Gemini, or we don't know the height of the
    // modal's content, then we render without Gemini.
    if (!props.useGemini || state.height == null) {
      return modalContent;
    }

    let geminiClasses = classNames('container-scrollable', props.geminiClass);
    let geminiContainerStyle = {
      height: state.height
    };

    return (
      <GeminiScrollbar
        autoshow={false}
        className={geminiClasses}
        style={geminiContainerStyle}>
        {modalContent}
      </GeminiScrollbar>
    );
  }

  getModal() {
    let {props, state} = this;
    let modalStyle = null;

    if (!props.open) {
      return null;
    }

    if (state.height != null) {
      modalStyle = {flexBasis: state.height};
    }

    return (

        {this.getCloseButton()}
        {this.getHeader()}

          {this.getModalContent()}

        {this.getFooter()}

    );
  }

  getBackdrop() {
    let {props} = this;

    if (!props.open) {
      return null;
    }

    return (

    );
  }

  render() {
    let {props} = this;
    let modalContent = null;

    if (props.open) {
      modalContent = (

          {this.getBackdrop()}
          {this.getModal()}

      );
    }

    return (
      <ReactCSSTransitionGroup
        transitionAppear={props.transitionAppear}
        transitionEnter={props.transitionEnter}
        transitionLeave={props.transitionLeave}
        transitionName={props.transitionNameModal}
        transitionAppearTimeout={props.transitionAppearTimeoutModal}
        transitionEnterTimeout={props.transitionEnterTimeoutModal}
        transitionLeaveTimeout={props.transitionLeaveTimeoutModal}
        component="div">
        {modalContent}
      </ReactCSSTransitionGroup>
    );
  }
}

ModalContents.defaultProps = {
  closeByBackdropClick: true,
  footer: null,
  header: null,
  onClose: () => {},
  open: false,
  showHeader: false,
  showFooter: false,
  subHeader: null,
  transitionNameModal: 'modal',
  transitionAppearTimeoutModal: 300,
  transitionEnterTimeoutModal: 300,
  transitionLeaveTimeoutModal: 300,
  transitionAppear: true,
  transitionEnter: true,
  transitionLeave: true,
  useGemini: true,

  // Default classes.
  backdropClass: 'modal-backdrop',
  bodyClass: 'modal-body-wrapper',
  closeButtonClass: 'modal-close',
  footerClass: 'modal-footer',
  headerClass: 'modal-header',
  modalClass: 'modal modal-large',
  scrollContainerClass: 'modal-body'
};

ModalContents.propTypes = {
  children: PropTypes.node,
  // Appends whatever value is provided as a close button.
  closeButton: PropTypes.node,
  // Allow closing of modal when click happens outside modal. Defaults to true.
  closeByBackdropClick: PropTypes.bool,
  // Allow resize of modal to fit screen. Defaults to true.
  // Optional footer
  footer: PropTypes.object,
  // Optional header.
  header: PropTypes.node,
  // Optional callback function exected when modal is closed.
  onClose: PropTypes.func,
  // True if modal is open, false otherwise.
  open: PropTypes.bool,
  // Set true to show header. Defaults to false.
  showHeader: PropTypes.bool,
  // Set true to show footer. Defaults to false.
  showFooter: PropTypes.bool,
  // Optional subheader.
  subHeader: PropTypes.node,
  // Optional enter and leave transition name for modal
  transitionNameModal: PropTypes.string,
  // Transition lengths, must be non-zero
  transitionAppearTimeoutModal: PropTypes.number,
  transitionEnterTimeoutModal: PropTypes.number,
  transitionLeaveTimeoutModal: PropTypes.number,
  // Optionally disable transitions
  transitionAppear: PropTypes.bool,
  transitionEnter: PropTypes.bool,
  transitionLeave: PropTypes.bool,
  // Option to use Gemini scrollbar. Defaults to true.
  useGemini: PropTypes.bool,

  // Classes
  backdropClass: PropTypes.string,
  bodyClass: PropTypes.string,
  closeButtonClass: PropTypes.string,
  footerClass: PropTypes.string,
  geminiClass: PropTypes.oneOfType([
    React.PropTypes.array,
    React.PropTypes.object,
    React.PropTypes.string
  ]),
  headerClass: PropTypes.string,
  modalClass: PropTypes.string,
  modalWrapperClass: PropTypes.string,
  scrollContainerClass: PropTypes.string
};

module.exports = ModalContents;
<|endoftext|>


<commit_before>"use strict";
var os = require('os');
var util = require('util');
var EventEmitter = require("events").EventEmitter;
var ffmpeg = require('fluent-ffmpeg');
var ytdl = require('ytdl-core');
var async = require('async');
var progress = require('progress-stream');

function YoutubeMp3Downloader(options) {

    var self = this;

    self.youtubeBaseUrl = 'http://www.youtube.com/watch?v=';
    self.youtubeVideoQuality = (options && options.youtubeVideoQuality ? options.youtubeVideoQuality : 'highest');
    self.outputPath = (options && options.outputPath ? options.outputPath : (os.platform() === 'win32' ? 'C:/Windows/Temp' : '/tmp'));
    self.queueParallelism = (options && options.queueParallelism ? options.queueParallelism : 1);
    self.progressTimeout = (options && options.progressTimeout ? options.progressTimeout : 1000);
    self.fileNameReplacements = [[/"/g, ''], [/'/g, ''], [/\//g, ''], [/\?/g, ''], [/:/g, ''], [/;/g, '']];

    if (options && options.ffmpegPath) {
        ffmpeg.setFfmpegPath(options.ffmpegPath);
    }

    //Async download/transcode queue
    self.downloadQueue = async.queue(function (task, callback) {

        self.emit("queueSize", self.downloadQueue.running() + self.downloadQueue.length());

        self.performDownload(task, function(err, result) {
            callback(err, result);
        });

    }, self.queueParallelism);

}

util.inherits(YoutubeMp3Downloader, EventEmitter);

YoutubeMp3Downloader.prototype.cleanFileName = function(fileName) {
    var self = this;

    self.fileNameReplacements.forEach(function(replacement) {
        fileName = fileName.replace(replacement[0], replacement[1]);
    });

    return fileName;
};

YoutubeMp3Downloader.prototype.download = function(videoId, fileName) {

    var self = this;
    var task = {
        videoId: videoId,
        fileName: fileName
    };

    self.downloadQueue.push(task, function (err, data) {
        if (err) {
            self.emit("error", err);
        } else {
            self.emit('finished', data);
            self.emit("queueSize", self.downloadQueue.running() + self.downloadQueue.length());
        }
    });

};

YoutubeMp3Downloader.prototype.performDownload = function(task, cb) {

    var self = this;
    var videoUrl = self.youtubeBaseUrl+task.videoId;

    ytdl.getInfo(videoUrl, function(err, info){

        var videoTitle = self.cleanFileName(info.title);
        var artist = "Unknown";
        var title = "Unknown";

        if (videoTitle.indexOf("-") > -1) {
            var temp = videoTitle.split("-");
            if (temp.length >= 2) {
                artist = temp[0].trim();
                title = temp[1].trim();
            }
        } else {
            title = videoTitle;
        }

        //Derive file name, if given, use it, if not, from video title
        var fileName = (task.fileName ? self.outputPath + '/' + task.fileName : self.outputPath + '/' + videoTitle + '.mp3');

        ytdl.getInfo(videoUrl, { quality: self.youtubeVideoQuality }, function(err, info) {

            //Stream setup
            var stream = ytdl.downloadFromInfo(info, {
                quality: self.youtubeVideoQuality
            });

            stream.on("response", function(httpResponse) {

                var resultObj = {};

                //Setup of progress module
                var str = progress({
                    length: parseInt(httpResponse.headers['content-length']),
                    time: self.progressTimeout
                });

                //Add progress event listener
                str.on('progress', function(progress) {
                    if (progress.percentage === 100) {
                        resultObj.stats= {
                            transferredBytes: progress.transferred,
                            runtime: progress.runtime,
                            averageSpeed: parseFloat(progress.speed.toFixed(2))
                        }
                    }
                    self.emit("progress", {videoId: task.videoId, progress: progress})
                });

                //Start encoding
                var proc = new ffmpeg({
                    source: stream.pipe(str)
                })
                .audioBitrate(info.formats[0].audioBitrate)
                .withAudioCodec('libmp3lame')
                .toFormat('mp3')
                .outputOptions('-id3v2_version', '4')
                .outputOptions('-metadata', 'title=' + title)
                .outputOptions('-metadata', 'artist=' + artist)
                .on('error', function(err) {
                    cb(err.message, null);
                })
                .on('end', function() {
                    resultObj.file =  fileName;
                    resultObj.videoId = task.videoId;
                    resultObj.youtubeUrl = videoUrl;
                    resultObj.videoTitle = videoTitle;
                    resultObj.artist = artist;
                    resultObj.title = title;
                    cb(null, resultObj);
                })
                .saveToFile(fileName);

            });

        });

    });

};

module.exports = YoutubeMp3Downloader;
<commit_msg>Fixed error handling for 404 errors and added support for thumbnails.<commit_after>"use strict";
var os = require('os');
var util = require('util');
var EventEmitter = require("events").EventEmitter;
var ffmpeg = require('fluent-ffmpeg');
var ytdl = require('ytdl-core');
var async = require('async');
var progress = require('progress-stream');

function YoutubeMp3Downloader(options) {

    var self = this;

    self.youtubeBaseUrl = 'http://www.youtube.com/watch?v=';
    self.youtubeVideoQuality = (options && options.youtubeVideoQuality ? options.youtubeVideoQuality : 'highest');
    self.outputPath = (options && options.outputPath ? options.outputPath : (os.platform() === 'win32' ? 'C:/Windows/Temp' : '/tmp'));
    self.queueParallelism = (options && options.queueParallelism ? options.queueParallelism : 1);
    self.progressTimeout = (options && options.progressTimeout ? options.progressTimeout : 1000);
    self.fileNameReplacements = [[/"/g, ''], [/'/g, ''], [/\//g, ''], [/\?/g, ''], [/:/g, ''], [/;/g, '']];

    if (options && options.ffmpegPath) {
        ffmpeg.setFfmpegPath(options.ffmpegPath);
    }

    //Async download/transcode queue
    self.downloadQueue = async.queue(function (task, callback) {

        self.emit("queueSize", self.downloadQueue.running() + self.downloadQueue.length());

        self.performDownload(task, function(err, result) {
            callback(err, result);
        });

    }, self.queueParallelism);

}

util.inherits(YoutubeMp3Downloader, EventEmitter);

YoutubeMp3Downloader.prototype.cleanFileName = function(fileName) {
    var self = this;

    self.fileNameReplacements.forEach(function(replacement) {
        fileName = fileName.replace(replacement[0], replacement[1]);
    });

    return fileName;
};

YoutubeMp3Downloader.prototype.download = function(videoId, fileName) {

    var self = this;
    var task = {
        videoId: videoId,
        fileName: fileName
    };

    self.downloadQueue.push(task, function (err, data) {
        if (err) {
            self.emit("error", err);
        } else {
            self.emit('finished', data);
            self.emit("queueSize", self.downloadQueue.running() + self.downloadQueue.length());
        }
    });

};

YoutubeMp3Downloader.prototype.performDownload = function(task, callback) {

    var self = this;
    var videoUrl = self.youtubeBaseUrl+task.videoId;

    ytdl.getInfo(videoUrl, function(err, info){
        if (err) {
          callback(err.message);
          return;
        }

        var videoTitle = self.cleanFileName(info.title);
        var artist = "Unknown";
        var title = "Unknown";
        var thumbnail = info.iurlhq;

        if (videoTitle.indexOf("-") > -1) {
            var temp = videoTitle.split("-");
            if (temp.length >= 2) {
                artist = temp[0].trim();
                title = temp[1].trim();
            }
        } else {
            title = videoTitle;
        }

        //Derive file name, if given, use it, if not, from video title
        var fileName = (task.fileName ? self.outputPath + '/' + task.fileName : self.outputPath + '/' + videoTitle + '.mp3');

        ytdl.getInfo(videoUrl, { quality: self.youtubeVideoQuality }, function(err, info) {

            //Stream setup
            var stream = ytdl.downloadFromInfo(info, {
                quality: self.youtubeVideoQuality
            });

            stream.on("response", function(httpResponse) {

                var resultObj = {};

                //Setup of progress module
                var str = progress({
                    length: parseInt(httpResponse.headers['content-length']),
                    time: self.progressTimeout
                });

                //Add progress event listener
                str.on('progress', function(progress) {
                    if (progress.percentage === 100) {
                        resultObj.stats= {
                            transferredBytes: progress.transferred,
                            runtime: progress.runtime,
                            averageSpeed: parseFloat(progress.speed.toFixed(2))
                        }
                    }
                    self.emit("progress", {videoId: task.videoId, progress: progress})
                });

                //Start encoding
                var proc = new ffmpeg({
                    source: stream.pipe(str)
                })
                .audioBitrate(info.formats[0].audioBitrate)
                .withAudioCodec('libmp3lame')
                .toFormat('mp3')
                .outputOptions('-id3v2_version', '4')
                .outputOptions('-metadata', 'title=' + title)
                .outputOptions('-metadata', 'artist=' + artist)
                .on('error', function(err) {
                    callback(err.message, null);
                })
                .on('end', function() {
                    resultObj.file =  fileName;
                    resultObj.videoId = task.videoId;
                    resultObj.youtubeUrl = videoUrl;
                    resultObj.videoTitle = videoTitle;
                    resultObj.artist = artist;
                    resultObj.title = title;
                    resultObj.thumbnail = thumbnail;
                    callback(null, resultObj);
                })
                .saveToFile(fileName);

            });

        });

    });

};

module.exports = YoutubeMp3Downloader;
<|endoftext|>


<commit_before>/* jslint node: true, esnext: true */
"use strict";

const chokidar = require('chokidar');

const fs = require('fs');
const path = require("path");

const BaseStep = require('kronos-step').Step;

const AdapterInboundFile = {
	"name": "kronos-adapter-inbound-file",
	"description": "Watches directories and opens a stream for new files matching the expected file name",
	"endpoints": {
		"inFileTrigger": {
			"in": true
		},
		"out": {
			"out": true
		}
	},

	initialize(manager, name, stepConfiguration, props) {

		// A regular expression. The file name must match this expression
		let regEx;
		if (stepConfiguration.regEx) {
			regEx = new RegExp(stepConfiguration.regEx);
		}
		props.regEx = {
			value: regEx
		};

		// an filter function. The function must have the signature
		// @param fileName The name of the file
		// function(fileName){}
		// If it returns a true vale the file will be read
		let filter;
		if (stepConfiguration.filter) {
			if (typeof stepConfiguration.filter === 'function') {
				filter = stepConfiguration.filter;
			} else {
				throw 'Filter must be a function';
			}
		}
		props.filter = {
			value: filter
		};

		// watch for changes on the directorys
		let watchDir;
		if (stepConfiguration.watchDir) {
			watchDir = stepConfiguration.watchDir;
		}
		props.watchDir = {
			value: watchDir
		};

		// if set to true, it will only file added new to the directory.
		// if false, it will start reading all the files in the directory
		let onlyReadNewFiles = true;
		if (stepConfiguration.onlyReadNewFiles !== undefined) {
			onlyReadNewFiles = stepConfiguration.onlyReadNewFiles;
		}
		props.onlyReadNewFiles = {
			value: onlyReadNewFiles
		};
	},

	/**
	 * Stops the watcher
	 * Overwrite to implement the functionality to bring the step into the stopped state.
	 * @return {Promise} default implementation always fullfills with the receiving step
	 */
	_stop() {
		if (this.watcher) {
			this.watcher.close();

			this.info({
				"short_message": "Stop watching directory"
			});

		}
		return Promise.resolve(this);
	},

	/**
	 * Starts the step
	 * Start the watcher which checks for changes in the given directory
	 */
	_start() {
		const self = this;
		if (self.watchDir) {
			let opt = {};
			if (self.onlyReadNewFiles) {
				opt.ignoreInitial = true;
			}

			self.info({
				"short_message": "Start watching directory",
				"directory": self.watchDir
			});

			self.watcher = chokidar.watch(self.watchDir, opt).on('add', function (filePath) {

				let absFileName = path.resolve(filePath);
				let fileDir = path.dirname(absFileName);
				let fileName = path.basename(absFileName);

				if (self.filter) {
					if (self.filter(fileName)) {
						self._createReadStreamMessage(absFileName, fileName, fileDir);
					}
				} else {
					if (self.regEx) {
						if (fileName.match(self.regEx)) {
							self._createReadStreamMessage(absFileName, fileName, fileDir);
						}
					} else {
						self._createReadStreamMessage(absFileName, fileName, fileDir);
					}
				}

			});
		}
		return Promise.resolve();
	},

	finalize(manager, stepConfiguration) {
		const self = this;
		const inEndpoint = self.endpoints.inFileTrigger;

		let generatorInitialized = false;

		inEndpoint.receive = function (message) {

			return new Promise(function (fulfill, reject) {
				if (message.payload) {
					if (typeof message.payload === 'string') {
						// take the payload as a single file name
						fulfill(self._parseInboundFileMessage(undefined, message.payload, message));
					} else if (Array.isArray(message.payload)) {
						// take it as an array of file names

						const promises = [];
						for (let i = 0; i < message.payload.length; i++) {
							// iterate over the files
							promises.push(self._parseInboundFileMessage(undefined, message.payload[i], message));
						}

						fulfill(Promise.all(promises));

					} else if (typeof message.payload === 'object') {
						if (message.payload.files) {
							let directory;
							if (message.payload.directory) {
								directory = message.payload.directory;
							}
							if (Array.isArray(message.payload.files)) {

								const promises = [];

								for (let i = 0; i < message.payload.files.length; i++) {
									// iterate over the files
									promises.push(self._parseInboundFileMessage(directory, message.payload.files[i], message));
								}

								fulfill(Promise.all(promises));
							} {
								const msg = {
									"message": message,
									"short_message": "The 'files' property of the payload object must be an array",
									"endpoint": "inFileTrigger"
								};
								self.error(msg);
								reject(msg);
							}
						}
						// take it as an array of strings
					} else {
						const msg = {
							"message": message,
							"short_message": "No matching payload in the message for this step",
							"endpoint": "inFileTrigger"
						};
						self.error(msg);
						reject(msg);
					}
				} else {
					const msg = {
						"message": message,
						"short_message": "No payload in the message",
						"endpoint": "inFileTrigger"
					};
					self.error(msg);
					reject(msg);
				}

			});
		};

	},

	/**
	 * Checks the inbound file message.
	 * Does the files exists and converst the file names and directoryies
	 * in a way that the normal function could handle it
	 * @return Promise
	 */
	_parseInboundFileMessage(directory, fileName, message) {
		let fileAbs;
		let fileDir;
		let fileRel;

		const self = this;

		return new Promise(function (fulfill, reject) {
			if (path.isAbsolute(fileName)) {
				// The given file name is absolute
				fileAbs = fileName;
				fileDir = path.dirname(fileName);
				fileRel = path.basename(fileName);
			} else {
				// the file name is relative
				if (directory) {
					// build the absolute name
					fileDir = directory;
					fileRel = fileName;
					fileAbs = path.resolve(path.join(directory, fileName));
				} else {
					// error for relative file names the directory is needed
					const msg = {
						"message": message,
						"short_message": "For reative file names the directory is mandatory",
						"endpoint": "inFileTrigger"
					};

					self.error(msg);
					reject(msg);
				}
			}

			if (fileAbs) {
				// check that the file exists

				fs.access(fileAbs, fs.R_OK, function (err) {
					if (err) {
						// write error log
						const msg = {
							"message": message,
							"short_message": `The file '${fileAbs}' does not exists`,
							"endpoint": "inFileTrigger"
						};
						self.error(msg);
						reject(msg);
					} else {
						// ok, trigger the file
						fulfill(self._createReadStreamMessage(fileAbs, fileRel, fileDir, message));
					}
				});

			} else {
				reject("unexpected ERROR");
			}
		});

	},

	/**
	 * Opens the readFileStream and pushes the event to the next step
	 * @param absFileName The absolute file name.
	 * @param fileName The file name without the path
	 * @param directory The directory the file is in
	 * @param messageToClone If the file comes from an external trigger, some message informations
	 * needs to be copied from the trigger message.
	 * @return Promise
	 */
	_createReadStreamMessage(absFileName, fileName, directory, messageToClone) {
		const self = this;
		const outEndpoint = self.endpoints.out;

		let stream = fs.createReadStream(absFileName);
		if (stream) {

			const message = {
				"info": {
					"file_name": fileName,
					"directory": directory
				}
			};

			message.payload = stream;

			// Get the file stats as sdditional information on the request
			return new Promise(function (fulfill, reject) {
				fs.stat(absFileName, function (err, stats) {
					if (err) {
						reject(err);
					} else {
						message.info.file_stat = stats;
						fulfill(
							outEndpoint.connected.receive(message, messageToClone));
					}
				});
			});

		}
	}

};

module.exports = Object.assign({}, BaseStep, AdapterInboundFile);
<commit_msg>style: use ' for strings<commit_after>/* jslint node: true, esnext: true */
'use strict';

const chokidar = require('chokidar');

const fs = require('fs');
const path = require('path');

const BaseStep = require('kronos-step').Step;

const AdapterInboundFile = {
	name: 'kronos-adapter-inbound-file',
	description: 'Watches directories and opens a stream for new files matching the expected file name',
	endpoints: {
		inFileTrigger: { in : true
		},
		out: {
			out: true
		}
	},

	initialize(manager, name, stepConfiguration, props) {

		// A regular expression. The file name must match this expression
		let regEx;
		if (stepConfiguration.regEx) {
			regEx = new RegExp(stepConfiguration.regEx);
		}
		props.regEx = {
			value: regEx
		};

		// an filter function. The function must have the signature
		// @param fileName The name of the file
		// function(fileName){}
		// If it returns a true vale the file will be read
		let filter;
		if (stepConfiguration.filter) {
			if (typeof stepConfiguration.filter === 'function') {
				filter = stepConfiguration.filter;
			} else {
				throw 'Filter must be a function';
			}
		}
		props.filter = {
			value: filter
		};

		// watch for changes on the directorys
		let watchDir;
		if (stepConfiguration.watchDir) {
			watchDir = stepConfiguration.watchDir;
		}
		props.watchDir = {
			value: watchDir
		};

		// if set to true, it will only file added new to the directory.
		// if false, it will start reading all the files in the directory
		let onlyReadNewFiles = true;
		if (stepConfiguration.onlyReadNewFiles !== undefined) {
			onlyReadNewFiles = stepConfiguration.onlyReadNewFiles;
		}
		props.onlyReadNewFiles = {
			value: onlyReadNewFiles
		};
	},

	/**
	 * Stops the watcher
	 * Overwrite to implement the functionality to bring the step into the stopped state.
	 * @return {Promise} default implementation always fullfills with the receiving step
	 */
	_stop() {
		if (this.watcher) {
			this.watcher.close();

			this.info({
				short_message: 'Stop watching directory'
			});

		}
		return Promise.resolve(this);
	},

	/**
	 * Starts the step
	 * Start the watcher which checks for changes in the given directory
	 */
	_start() {
		const self = this;
		if (self.watchDir) {
			let opt = {};
			if (self.onlyReadNewFiles) {
				opt.ignoreInitial = true;
			}

			self.info({
				short_message: 'Start watching directory',
				directory: self.watchDir
			});

			self.watcher = chokidar.watch(self.watchDir, opt).on('add', function (filePath) {

				let absFileName = path.resolve(filePath);
				let fileDir = path.dirname(absFileName);
				let fileName = path.basename(absFileName);

				if (self.filter) {
					if (self.filter(fileName)) {
						self._createReadStreamMessage(absFileName, fileName, fileDir);
					}
				} else {
					if (self.regEx) {
						if (fileName.match(self.regEx)) {
							self._createReadStreamMessage(absFileName, fileName, fileDir);
						}
					} else {
						self._createReadStreamMessage(absFileName, fileName, fileDir);
					}
				}

			});
		}
		return Promise.resolve();
	},

	finalize(manager, stepConfiguration) {
		const self = this;
		const inEndpoint = self.endpoints.inFileTrigger;

		let generatorInitialized = false;

		inEndpoint.receive = function (message) {

			return new Promise(function (fulfill, reject) {
				if (message.payload) {
					if (typeof message.payload === 'string') {
						// take the payload as a single file name
						fulfill(self._parseInboundFileMessage(undefined, message.payload, message));
					} else if (Array.isArray(message.payload)) {
						// take it as an array of file names

						const promises = [];
						for (let i = 0; i < message.payload.length; i++) {
							// iterate over the files
							promises.push(self._parseInboundFileMessage(undefined, message.payload[i], message));
						}

						fulfill(Promise.all(promises));

					} else if (typeof message.payload === 'object') {
						if (message.payload.files) {
							let directory;
							if (message.payload.directory) {
								directory = message.payload.directory;
							}
							if (Array.isArray(message.payload.files)) {

								const promises = [];

								for (let i = 0; i < message.payload.files.length; i++) {
									// iterate over the files
									promises.push(self._parseInboundFileMessage(directory, message.payload.files[i], message));
								}

								fulfill(Promise.all(promises));
							} {
								const msg = {
									message: message,
									short_message: "The 'files' property of the payload object must be an array",
									endpoint: 'inFileTrigger'
								};
								self.error(msg);
								reject(msg);
							}
						}
						// take it as an array of strings
					} else {
						const msg = {
							message: message,
							short_message: 'No matching payload in the message for this step',
							endpoint: 'inFileTrigger'
						};
						self.error(msg);
						reject(msg);
					}
				} else {
					const msg = {
						message: message,
						short_message: 'No payload in the message',
						endpoint: 'inFileTrigger'
					};
					self.error(msg);
					reject(msg);
				}

			});
		};

	},

	/**
	 * Checks the inbound file message.
	 * Does the files exists and converst the file names and directoryies
	 * in a way that the normal function could handle it
	 * @return Promise
	 */
	_parseInboundFileMessage(directory, fileName, message) {
		let fileAbs;
		let fileDir;
		let fileRel;

		const self = this;

		return new Promise(function (fulfill, reject) {
			if (path.isAbsolute(fileName)) {
				// The given file name is absolute
				fileAbs = fileName;
				fileDir = path.dirname(fileName);
				fileRel = path.basename(fileName);
			} else {
				// the file name is relative
				if (directory) {
					// build the absolute name
					fileDir = directory;
					fileRel = fileName;
					fileAbs = path.resolve(path.join(directory, fileName));
				} else {
					// error for relative file names the directory is needed
					const msg = {
						message: message,
						short_message: 'For reative file names the directory is mandatory',
						endpoint: 'inFileTrigger'
					};

					self.error(msg);
					reject(msg);
				}
			}

			if (fileAbs) {
				// check that the file exists

				fs.access(fileAbs, fs.R_OK, function (err) {
					if (err) {
						// write error log
						const msg = {
							message: message,
							short_message: `The file '${fileAbs}' does not exists`,
							endpoint: 'inFileTrigger'
						};
						self.error(msg);
						reject(msg);
					} else {
						// ok, trigger the file
						fulfill(self._createReadStreamMessage(fileAbs, fileRel, fileDir, message));
					}
				});

			} else {
				reject('unexpected ERROR');
			}
		});

	},

	/**
	 * Opens the readFileStream and pushes the event to the next step
	 * @param absFileName The absolute file name.
	 * @param fileName The file name without the path
	 * @param directory The directory the file is in
	 * @param messageToClone If the file comes from an external trigger, some message informations
	 * needs to be copied from the trigger message.
	 * @return Promise
	 */
	_createReadStreamMessage(absFileName, fileName, directory, messageToClone) {
		const self = this;
		const outEndpoint = self.endpoints.out;

		let stream = fs.createReadStream(absFileName);
		if (stream) {

			const message = {
				'info': {
					'file_name': fileName,
					'directory': directory
				}
			};

			message.payload = stream;

			// Get the file stats as sdditional information on the request
			return new Promise(function (fulfill, reject) {
				fs.stat(absFileName, function (err, stats) {
					if (err) {
						reject(err);
					} else {
						message.info.file_stat = stats;
						fulfill(
							outEndpoint.connected.receive(message, messageToClone));
					}
				});
			});

		}
	}

};

module.exports = Object.assign({}, BaseStep, AdapterInboundFile);
<|endoftext|>


<commit_before>"use strict";

var toReference = require('./helpers/to-reference');
var isArray = require('./helpers/is-array');

function insertVariable(t, index) {
	return t.ObjectProperty(t.identifier('tag'), t.ObjectExpression(
		[t.ObjectProperty(t.identifier('index'), t.NumericLiteral(index))]
	));
}

function constructTemplateChildren(t, children) {
	if (isArray(children)) {
		if (children.length > 1) {
			return t.ArrayExpression(
				children.map(function(child) {
					return constructTemplateNode(t, child);
				})
			);
		} else {
			return constructTemplateNode(t, children[0]);
		}
	} else {
		return constructTemplateNode(children);
	}
}

function getObjectValue(t, val) {
	var objectValue = t.NullLiteral;

	if (val == null) {
		objectValue = t.identifier('null');
	} else if (val.index !== undefined) {
		objectValue = t.identifier('v' + val.index);
	} else if (typeof val === 'string') {
		objectValue = t.StringLiteral(val);
	} else if (typeof val === 'number') {
		objectValue = t.NumericLiteral(val);
	} else if (typeof val === 'boolean') {
		objectValue = t.BooleanLiteral(val);
	} else {
		objectValue = val;
	}

	return objectValue;
}

function constructTemplateNode(t, node, isRoot) {
	if (node == null) {
		return null;
	}
	if (node.index !== undefined) {
		return t.identifier('v' + node.index);
	}

	var tag = node.tag;

	if (tag) {
		var template = [];

		if (tag.index !== undefined) {
			template.push(t.ObjectProperty(t.identifier('tag'), t.identifier('v' + tag.index)))
		} else {
			template.push(t.ObjectProperty(t.identifier('tag'), t.StringLiteral(tag)))
		}
		var attrs = node.attrs;

		if (attrs != null) {
			if (attrs.index !== undefined) {
				template.push(t.ObjectProperty(t.identifier('attrs'), t.identifier('v' + attrs.index)));
			} else  {
				var attrKeys = Object.keys(attrs);

				if (attrs.key && isRoot) {
					template.push(t.ObjectProperty(t.identifier('key'), getObjectValue(t, attrs.key)))
					delete attrs.key;
				}
				if (attrKeys.length > 0) {
					template.push(t.ObjectProperty(t.identifier('attrs'), t.ObjectExpression(attrKeys.map(function (attrKey) {
						var id = /[\W]+/.test(attrKey) ? "'" + attrKey + "'" : attrKey;
						return t.ObjectProperty(t.identifier(id), getObjectValue(t, attrs[attrKey]));
					}))));
				}
			}
		}
		var children = node.children;

		if (children != null) {
			var childrenItems = constructTemplateChildren(t, children);

			if (childrenItems !== null) {
				template.push(t.ObjectProperty(t.identifier('children'), childrenItems));
			}
		}

		return t.ObjectExpression(template);
	} else {
		if (typeof node === 'string') {
			return t.StringLiteral(node);
		} else if (typeof node === 'number') {
			return t.NumericLiteral(node);
		} else if (typeof node === 'boolean') {
			return t.BooleanLiteral(node);
		} else {
			return node;
		}
	}
}

module.exports = function addTemplatesToModule(t, toInsert, templateKey, root) {
	var node = toInsert.node;
	var index = toInsert.index;

	node.body.splice(index, 0,
		t.variableDeclaration("var", [
			t.variableDeclarator(t.identifier(templateKey),
			t.callExpression(t.memberExpression(t.identifier("Inferno"), t.identifier("createTemplate")), [
				t.FunctionExpression(null, root.templateValues.map(function(val, i) { return toReference(t, "v" + i) }), t.BlockStatement([
					t.ReturnStatement(constructTemplateNode(t, root.templateElem, true))
				]))
			]))
		])
	);
}
<commit_msg>Added support for data-* and aria-* attributes<commit_after>"use strict";

var toReference = require('./helpers/to-reference');
var isArray = require('./helpers/is-array');

function insertVariable(t, index) {
	return t.ObjectProperty(t.identifier('tag'), t.ObjectExpression(
		[t.ObjectProperty(t.identifier('index'), t.NumericLiteral(index))]
	));
}

function constructTemplateChildren(t, children) {
	if (isArray(children)) {
		if (children.length > 1) {
			return t.ArrayExpression(
				children.map(function(child) {
					return constructTemplateNode(t, child);
				})
			);
		} else {
			return constructTemplateNode(t, children[0]);
		}
	} else {
		return constructTemplateNode(children);
	}
}

function getObjectValue(t, val) {
	var objectValue = t.NullLiteral;

	if (val == null) {
		objectValue = t.identifier('null');
	} else if (val.index !== undefined) {
		objectValue = t.identifier('v' + val.index);
	} else if (typeof val === 'string') {
		objectValue = t.StringLiteral(val);
	} else if (typeof val === 'number') {
		objectValue = t.NumericLiteral(val);
	} else if (typeof val === 'boolean') {
		objectValue = t.BooleanLiteral(val);
	} else {
		objectValue = val;
	}

	return objectValue;
}

function constructTemplateNode(t, node, isRoot) {
	if (node == null) {
		return null;
	}
	if (node.index !== undefined) {
		return t.identifier('v' + node.index);
	}

	var tag = node.tag;

	if (tag) {
		var template = [];

		if (tag.index !== undefined) {
			template.push(t.ObjectProperty(t.identifier('tag'), t.identifier('v' + tag.index)))
		} else {
			template.push(t.ObjectProperty(t.identifier('tag'), t.StringLiteral(tag)))
		}
		var attrs = node.attrs;

		if (attrs != null) {
			if (attrs.index !== undefined) {
				template.push(t.ObjectProperty(t.identifier('attrs'), t.identifier('v' + attrs.index)));
			} else  {
				var attrKeys = Object.keys(attrs);

				if (attrs.key && isRoot) {
					template.push(t.ObjectProperty(t.identifier('key'), getObjectValue(t, attrs.key)))
					delete attrs.key;
				}
				if (attrKeys.length > 0) {
					template.push(t.ObjectProperty(t.identifier('attrs'), t.ObjectExpression(attrKeys.map(function (attrKey) {
						var proposedAttribute = attrKey;
						if (/^(data|aria)/.test(proposedAttribute)){
							proposedAttribute = attrKey.replace(/([A-Z])/g, function(str, match, index) {
								return index ? '-' + match : match;
							}).toLowerCase()
						}
						var id = /[\W]+/.test(proposedAttribute) ? "'" + proposedAttribute + "'" : proposedAttribute;
						return t.ObjectProperty(t.identifier(id), getObjectValue(t, attrs[attrKey]));
					}))));
				}
			}
		}
		var children = node.children;

		if (children != null) {
			var childrenItems = constructTemplateChildren(t, children);

			if (childrenItems !== null) {
				template.push(t.ObjectProperty(t.identifier('children'), childrenItems));
			}
		}

		return t.ObjectExpression(template);
	} else {
		if (typeof node === 'string') {
			return t.StringLiteral(node);
		} else if (typeof node === 'number') {
			return t.NumericLiteral(node);
		} else if (typeof node === 'boolean') {
			return t.BooleanLiteral(node);
		} else {
			return node;
		}
	}
}

module.exports = function addTemplatesToModule(t, toInsert, templateKey, root) {
	var node = toInsert.node;
	var index = toInsert.index;

	node.body.splice(index, 0,
		t.variableDeclaration("var", [
			t.variableDeclarator(t.identifier(templateKey),
			t.callExpression(t.memberExpression(t.identifier("Inferno"), t.identifier("createTemplate")), [
				t.FunctionExpression(null, root.templateValues.map(function(val, i) { return toReference(t, "v" + i) }), t.BlockStatement([
					t.ReturnStatement(constructTemplateNode(t, root.templateElem, true))
				]))
			]))
		])
	);
}
<|endoftext|>


<commit_before>'use strict';

import core from 'bower:metal/src/core';
import Ajax from 'bower:metal-ajax/src/Ajax';
import MultiMap from 'bower:metal-multimap/src/MultiMap';
import CancellablePromise from 'bower:metal-promise/src/promise/Promise';
import Screen from './Screen';

class RequestScreen extends Screen {

	/**
	 * Request screen abstract class to perform io operations on descendant
	 * screens.
	 * @constructor
	 * @extends {Screen}
	 */
	constructor() {
		super();

		/**
		 * @inheritDoc
		 * @default true
		 */
		this.cacheable = true;

		/**
		 * Holds default http headers to set on request.
		 * @type {?Object=}
		 * @default {
		 *   'X-PJAX': 'true',
		 *   'X-Requested-With': 'XMLHttpRequest'
		 * }
		 * @protected
		 */
		this.httpHeaders = {
			'X-PJAX': 'true',
			'X-Requested-With': 'XMLHttpRequest'
		};

		/**
		 * Holds default http method to perform the request.
		 * @type {!string}
		 * @default RequestScreen.GET
		 * @protected
		 */
		this.httpMethod = RequestScreen.GET;

		/**
		 * Holds the XHR object responsible for the request.
		 * @type {XMLHttpRequest}
		 * @default null
		 * @protected
		 */
		this.request = null;

		/**
		 * Holds the request timeout in milliseconds.
		 * @type {!number}
		 * @default 30000
		 * @protected
		 */
		this.timeout = 30000;
	}

	/**
	 * Gets the http headers.
	 * @return {?Object=}
	 */
	getHttpHeaders() {
		return this.httpHeaders;
	}

	/**
	 * Gets the http method.
	 * @return {!string}
	 */
	getHttpMethod() {
		return this.httpMethod;
	}

	/**
	 * Gets the request object.
	 * @return {?Object}
	 */
	getRequest() {
		return this.request;
	}

	/**
	 * Gets the request timeout.
	 * @return {!number}
	 */
	getTimeout() {
		return this.timeout;
	}

	/**
	 * @inheritDoc
	 */
	load(path) {
		var cache = this.getCache();
		if (core.isDefAndNotNull(cache)) {
			return CancellablePromise.resolve(cache);
		}

		var httpMethod = this.httpMethod;
		var headers = new MultiMap();

		Object.keys(this.httpHeaders).forEach(header => headers.add(header, this.httpHeaders[header]));

		return Ajax
			.request(path, this.httpMethod, null, headers, null, this.timeout)
			.then(xhr => {
				this.setRequest(xhr);
				if (httpMethod === RequestScreen.GET && this.isCacheable()) {
					this.addCache(xhr.responseText);
				}
				return xhr.responseText;
			});
	}

	/**
	 * Sets the http headers.
	 * @param {?Object=} httpHeaders
	 */
	setHttpHeaders(httpHeaders) {
		this.httpHeaders = httpHeaders;
	}

	/**
	 * Sets the http method.
	 * @param {!string} httpMethod
	 */
	setHttpMethod(httpMethod) {
		this.httpMethod = httpMethod.toLowerCase();
	}

	/**
	 * Sets the request object.
	 * @param {?Object} request
	 */
	setRequest(request) {
		this.request = request;
	}

	/**
	 * Sets the request timeout in milliseconds.
	 * @param {!number} timeout
	 */
	setTimeout(timeout) {
		this.timeout = timeout;
	}

}

/**
 * Holds value for method get.
 * @type {string}
 * @default 'get'
 * @static
 */
RequestScreen.GET = 'get';

/**
 * Holds value for method post.
 * @type {string}
 * @default 'post'
 * @static
 */
RequestScreen.POST = 'post';

export default RequestScreen;<commit_msg>RequestScreen must store last request<commit_after>'use strict';

import core from 'bower:metal/src/core';
import Ajax from 'bower:metal-ajax/src/Ajax';
import MultiMap from 'bower:metal-multimap/src/MultiMap';
import CancellablePromise from 'bower:metal-promise/src/promise/Promise';
import Screen from './Screen';

class RequestScreen extends Screen {

	/**
	 * Request screen abstract class to perform io operations on descendant
	 * screens.
	 * @constructor
	 * @extends {Screen}
	 */
	constructor() {
		super();

		/**
		 * @inheritDoc
		 * @default true
		 */
		this.cacheable = true;

		/**
		 * Holds default http headers to set on request.
		 * @type {?Object=}
		 * @default {
		 *   'X-PJAX': 'true',
		 *   'X-Requested-With': 'XMLHttpRequest'
		 * }
		 * @protected
		 */
		this.httpHeaders = {
			'X-PJAX': 'true',
			'X-Requested-With': 'XMLHttpRequest'
		};

		/**
		 * Holds default http method to perform the request.
		 * @type {!string}
		 * @default RequestScreen.GET
		 * @protected
		 */
		this.httpMethod = RequestScreen.GET;

		/**
		 * Holds the XHR object responsible for the request.
		 * @type {XMLHttpRequest}
		 * @default null
		 * @protected
		 */
		this.request = null;

		/**
		 * Holds the request timeout in milliseconds.
		 * @type {!number}
		 * @default 30000
		 * @protected
		 */
		this.timeout = 30000;
	}

	/**
	 * Gets the http headers.
	 * @return {?Object=}
	 */
	getHttpHeaders() {
		return this.httpHeaders;
	}

	/**
	 * Gets the http method.
	 * @return {!string}
	 */
	getHttpMethod() {
		return this.httpMethod;
	}

	/**
	 * Gets the request object.
	 * @return {?Object}
	 */
	getRequest() {
		return this.request;
	}

	/**
	 * Gets the request timeout.
	 * @return {!number}
	 */
	getTimeout() {
		return this.timeout;
	}

	/**
	 * @inheritDoc
	 */
	load(path) {
		var cache = this.getCache();
		if (core.isDefAndNotNull(cache)) {
			return CancellablePromise.resolve(cache);
		}

		var httpMethod = this.httpMethod;
		var headers = new MultiMap();

		Object.keys(this.httpHeaders).forEach(header => headers.add(header, this.httpHeaders[header]));

		return Ajax
			.request(path, this.httpMethod, null, headers, null, this.timeout)
			.then(xhr => {
				this.setRequest(xhr);
				if (httpMethod === RequestScreen.GET && this.isCacheable()) {
					this.addCache(xhr.responseText);
				}
				this.setRequest(xhr);
				return xhr.responseText;
			});
	}

	/**
	 * Sets the http headers.
	 * @param {?Object=} httpHeaders
	 */
	setHttpHeaders(httpHeaders) {
		this.httpHeaders = httpHeaders;
	}

	/**
	 * Sets the http method.
	 * @param {!string} httpMethod
	 */
	setHttpMethod(httpMethod) {
		this.httpMethod = httpMethod.toLowerCase();
	}

	/**
	 * Sets the request object.
	 * @param {?Object} request
	 */
	setRequest(request) {
		this.request = request;
	}

	/**
	 * Sets the request timeout in milliseconds.
	 * @param {!number} timeout
	 */
	setTimeout(timeout) {
		this.timeout = timeout;
	}

}

/**
 * Holds value for method get.
 * @type {string}
 * @default 'get'
 * @static
 */
RequestScreen.GET = 'get';

/**
 * Holds value for method post.
 * @type {string}
 * @default 'post'
 * @static
 */
RequestScreen.POST = 'post';

export default RequestScreen;<|endoftext|>