Skip to content

Commit

Permalink
#119 Implementation of browser specific XHR-Reader
Browse files Browse the repository at this point in the history
  • Loading branch information
Borewit committed Jul 18, 2018
1 parent 31e3eb6 commit 8dc0aaa
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 179 deletions.
98 changes: 98 additions & 0 deletions src/MusicMetadataParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {CombinedTagMapper, IAudioMetadata, INativeAudioMetadata, IOptions} from "./";
import {TagPriority, TagType} from "./common/GenericTagTypes";
import {ParserFactory} from "./ParserFactory";
import {Promise} from "es6-promise";
import {ITokenizer} from "strtok3";

export class MusicMetadataParser {

public static joinArtists(artists: string[]): string {
if (artists.length > 2) {
return artists.slice(0, artists.length - 1).join(', ') + ' & ' + artists[artists.length - 1];
}
return artists.join(' & ');
}

private tagMapper = new CombinedTagMapper();

/**
* Parse data provided by tokenizer.
* @param {ITokenizer} tokenizer feed input (abstracting input)
* @param {IOptions} options
* @returns {Promise<IAudioMetadata>}
*/
public parse(tokenizer: ITokenizer, options?: IOptions): Promise<IAudioMetadata> {
if (!tokenizer.fileSize && options.fileSize) {
tokenizer.fileSize = options.fileSize;
}
return ParserFactory.parse(tokenizer, options).then(nativeTags => {
return this.parseNativeTags(nativeTags, options.native, options.mergeTagHeaders);
});
}

/**
* Convert native tags to common tags
* @param nativeData
* @param includeNative return native tags in result
* @param mergeTagHeaders Merge tah headers
* @returns {IAudioMetadata} Native + common tags
*/
public parseNativeTags(nativeData: INativeAudioMetadata, includeNative?: boolean, mergeTagHeaders?: boolean): IAudioMetadata {

const metadata: IAudioMetadata = {
format: nativeData.format,
native: includeNative ? nativeData.native : undefined,
common: {} as any
};

metadata.format.tagTypes = [];

for (const tagType in nativeData.native) {
metadata.format.tagTypes.push(tagType as TagType);
}

for (const tagType of TagPriority) {

if (nativeData.native[tagType]) {
if (nativeData.native[tagType].length === 0) {
// ToDo: register warning: empty tag header
} else {

const common = {
track: {no: null, of: null},
disk: {no: null, of: null}
};

for (const tag of nativeData.native[tagType]) {
this.tagMapper.setGenericTag(common, tagType as TagType, tag);
}

for (const tag of Object.keys(common)) {
if (!metadata.common[tag]) {
metadata.common[tag] = common[tag];
}
}

if (!mergeTagHeaders) {
break;
}
}
}
}

if (metadata.common.artists && metadata.common.artists.length > 0) {
// common.artists explicitly by meta-data
metadata.common.artist = !metadata.common.artist ? MusicMetadataParser.joinArtists(metadata.common.artists) : metadata.common.artist[0];
} else {
if (metadata.common.artist) {
metadata.common.artists = metadata.common.artist as any;
if (metadata.common.artist.length > 1) {
delete metadata.common.artist;
} else {
metadata.common.artist = metadata.common.artist[0];
}
}
}
return metadata;
}
}
76 changes: 20 additions & 56 deletions src/ParserFactory.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {INativeAudioMetadata, IOptions} from "./";
import * as strtok3 from "strtok3";
import * as Stream from "stream";
import * as path from "path";
import {ITokenizer} from "strtok3";
import * as fileType from "file-type";
import * as MimeType from "media-typer";
import {Promise} from "es6-promise";
Expand All @@ -11,74 +9,40 @@ import * as _debug from "debug";
const debug = _debug("music-metadata:parser:factory");

export interface ITokenParser {
parse(tokenizer: strtok3.ITokenizer, options: IOptions): Promise<INativeAudioMetadata>;
parse(tokenizer: ITokenizer, options: IOptions): Promise<INativeAudioMetadata>;
}

export class ParserFactory {

/**
* Extract metadata from the given audio file
* @param filePath File path of the audio file to parse
* @param opts
* .fileSize=true Return filesize
* .native=true Will return original header in result
* @returns {Promise<INativeAudioMetadata>}
*/
public static parseFile(filePath: string, opts: IOptions = {}): Promise<INativeAudioMetadata> {

return strtok3.fromFile(filePath).then(fileTokenizer => {
const parserName = ParserFactory.getParserIdForExtension(filePath);
if (parserName) {
return ParserFactory.loadParser(parserName, opts).then(parser => {
return parser.parse(fileTokenizer, opts).then(metadata => {
return fileTokenizer.close().then(() => {
return metadata;
});
}).catch(err => {
return fileTokenizer.close().then(() => {
throw err;
});
});
});

} else {
throw new Error('No parser found for extension: ' + path.extname(filePath));
}
});
}

/**
* Parse metadata from stream
* @param stream Node stream
* @param mimeType The mime-type, e.g. "audio/mpeg", extension e.g. ".mp3" or filename. This is used to redirect to the correct parser.
* @param opts Parsing options
* @returns {Promise<INativeAudioMetadata>}
*/
public static parseStream(stream: Stream.Readable, mimeType: string, opts: IOptions = {}): Promise<INativeAudioMetadata> {

return strtok3.fromStream(stream).then(tokenizer => {
if (!tokenizer.fileSize && opts.fileSize) {
tokenizer.fileSize = opts.fileSize;
}
return this.parse(tokenizer, mimeType, opts);
});
}

/**
* Parse metadata from tokenizer
* @param {ITokenizer} tokenizer
* @param {string} contentType
* @param {IOptions} opts
* @returns {Promise<INativeAudioMetadata>}
*/
public static parse(tokenizer: strtok3.ITokenizer, contentType: string, opts: IOptions = {}): Promise<INativeAudioMetadata> {
public static parse(tokenizer: ITokenizer, opts: IOptions = {}): Promise<INativeAudioMetadata> {

// Resolve parser based on MIME-type or file extension
let parserId = ParserFactory.getParserIdForMimeType(contentType) || ParserFactory.getParserIdForExtension(contentType);
let parserId;

if (opts.contentType) {
parserId = ParserFactory.getParserIdForMimeType(opts.contentType) || ParserFactory.getParserIdForExtension(opts.contentType);
if (!parserId) {
debug("No parser found for MIME-type:" + opts.contentType);
}
}

if (!parserId && opts.path) {
parserId = ParserFactory.getParserIdForExtension(opts.path);
if (!parserId) {
debug("No parser found for path:" + opts.path);
}
}

if (!parserId) {
// No MIME-type mapping found
debug("No parser found for MIME-type / extension:" + contentType);
debug("Try to determine type based content...");

const buf = Buffer.alloc(4100);
return tokenizer.peekBuffer(buf).then(() => {
Expand Down Expand Up @@ -108,7 +72,7 @@ export class ParserFactory {
if (!filePath)
return;

const extension = path.extname(filePath).toLocaleLowerCase() || filePath;
const extension = filePath.slice((filePath.lastIndexOf(".") - 1 >>> 0) + 1) || filePath;

switch (extension) {

Expand Down
35 changes: 35 additions & 0 deletions src/browser/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {IAudioMetadata, IOptions} from "../";
import {Promise} from "es6-promise";
import * as strtok3 from "strtok3/lib/browser";
import {MusicMetadataParser} from "../MusicMetadataParser";
import * as Stream from "stream";

/**
* Get audio track using XML-HTTP-Request (XHR).
* @param {string} url URL to read from
* @param {IOptions} opts Options
* @returns {Promise<IAudioMetadata>}
*/
export function parseUrl(url: string, opts?: IOptions): Promise<IAudioMetadata> {

return strtok3.fromUrl(url).then(tokenizer => {
opts.contentType = tokenizer.contentType || opts.contentType;
return new MusicMetadataParser().parse(tokenizer, opts); // ToDo: mime type
});
}

/**
* Parse audio from stream
* @param stream Node stream
* @param mimeType The mime-type, e.g. "audio/mpeg", extension e.g. ".mp3" or filename. This is used to redirect to the correct parser.
* @param opts Parsing options
* .native=true Will return original header in result
* .mergeTagHeaders=false Populate common from data of all headers available
* @returns {Promise<IAudioMetadata>}
*/
export function parseStream(stream: Stream.Readable, mimeType?: string, opts?: IOptions): Promise<IAudioMetadata> {
opts.contentType = mimeType || opts.contentType;
return strtok3.fromStream(stream).then(tokenizer => {
return new MusicMetadataParser().parse(tokenizer, opts);
});
}
Loading

0 comments on commit 8dc0aaa

Please sign in to comment.