Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
175 lines
5.3 KiB
JavaScript
175 lines
5.3 KiB
JavaScript
/* global LazyLoader, VorbisPictureComment */
|
|
/* exported FLACMetadata */
|
|
'use strict';
|
|
|
|
/**
|
|
* Parse metadata for FLAC files (this is very similar to Ogg files, but with a
|
|
* different container format).
|
|
*
|
|
* Format information:
|
|
* http://flac.sourceforge.net/format.html
|
|
*/
|
|
var FLACMetadata = (function() {
|
|
// Fields that should be stored as integers, not strings
|
|
var INTFIELDS = [
|
|
'tracknum', 'trackcount', 'discnum', 'disccount'
|
|
];
|
|
|
|
// Map FLAC field names to metadata property names
|
|
var FLACFIELDS = {
|
|
title: 'title',
|
|
artist: 'artist',
|
|
album: 'album',
|
|
tracknumber: 'tracknum',
|
|
tracktotal: 'trackcount',
|
|
discnumber: 'discnum',
|
|
disctotal: 'disccount'
|
|
};
|
|
|
|
/**
|
|
* Parse a file and return a Promise with the metadata.
|
|
*
|
|
* @param {BlobView} blobview The audio file to parse.
|
|
* @return {Promise} A Promise returning the parsed metadata object.
|
|
*/
|
|
function parse(blobview) {
|
|
// First four bytes are "fLaC" or we wouldn't be here.
|
|
blobview.seek(4);
|
|
|
|
var metadata = {tag_format: 'flac'};
|
|
var has_vorbis_comment = false;
|
|
var has_picture = false;
|
|
|
|
function processBlock(block) {
|
|
if (block.block_type === 4) {
|
|
readAllComments(block.view, metadata);
|
|
has_vorbis_comment = true;
|
|
} else if (block.block_type === 6) {
|
|
metadata.picture = VorbisPictureComment.readPicFrame(block.view);
|
|
has_picture = true;
|
|
}
|
|
|
|
return (!has_vorbis_comment || !has_picture);
|
|
}
|
|
|
|
return LazyLoader.load('js/metadata/vorbis_picture.js').then(() => {
|
|
return findMetadataBlocks(blobview, processBlock);
|
|
}).then(() => {
|
|
return metadata;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Step over metadata blocks until we find the proper metadata block.
|
|
*
|
|
* @param {BlobView} blobview The BlobView for the file.
|
|
* @param {function} callback The callback to process the block with.
|
|
* @return {Promise} A Promise that resolves when all relevant metadata blocks
|
|
* have been processed.
|
|
*/
|
|
function findMetadataBlocks(blobview, callback) {
|
|
return readMetadataBlockHeader(blobview).then((block) => {
|
|
if (callback(block) && !block.last) {
|
|
block.view.advance(block.length - block.view.index);
|
|
findMetadataBlocks(block.view, callback);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read a metadata block header and fetch its contents from the Blob, plus
|
|
* enough extra data to read the next block's header.
|
|
*
|
|
* @param {BlobView} blobview The BlobView for the file.
|
|
* @return {Promise} A Promise resolving to an object with the following
|
|
* fields:
|
|
* {Boolean} last True if this is the last metadata block.
|
|
* {Number} block_type The block's type, as an integer.
|
|
* {Number} length The size in bytes of the block's body (excluding the
|
|
* header).
|
|
* {BlobView} view The BlobView with the block's data, starting at the
|
|
* beginning of the metadata block's content (not the header).
|
|
*/
|
|
function readMetadataBlockHeader(blobview) {
|
|
return new Promise(function(resolve, reject) {
|
|
var header = blobview.readUnsignedByte();
|
|
var last = (header & 0x80) === 0x80;
|
|
var block_type = header & 0x7F;
|
|
var length = blobview.readUint24(false);
|
|
|
|
// Get the contents of this block, plus enough to read the next block's
|
|
// header if necessary.
|
|
blobview.getMore(blobview.viewOffset + blobview.index, length + 4,
|
|
function(more, err) {
|
|
if (err) {
|
|
reject(err);
|
|
return;
|
|
}
|
|
|
|
resolve({
|
|
last: last,
|
|
block_type: block_type,
|
|
length: length,
|
|
view: more
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Read all the comments in a metadata header block.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
* @param {Metadata} metadata The (partially filled-in) metadata object.
|
|
*/
|
|
function readAllComments(page, metadata) {
|
|
var vendor_string_length = page.readUnsignedInt(true);
|
|
page.advance(vendor_string_length); // skip vendor string
|
|
|
|
var num_comments = page.readUnsignedInt(true);
|
|
for (var i = 0; i < num_comments; i++) {
|
|
try {
|
|
var comment = readComment(page);
|
|
if (comment) {
|
|
if (!(comment.field in metadata)) {
|
|
metadata[comment.field] = comment.value;
|
|
} else {
|
|
// We already have a value, so append this new one.
|
|
metadata[comment.field] += ' / ' + comment.value;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Error parsing vorbis comment', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read a single comment field.
|
|
*
|
|
* @param {BlobView} page The audio file being parsed.
|
|
*/
|
|
function readComment(page) {
|
|
var comment_length = page.readUnsignedInt(true);
|
|
var comment = page.readUTF8Text(comment_length);
|
|
var equal = comment.indexOf('=');
|
|
if (equal === -1) {
|
|
throw new Error('missing delimiter in comment');
|
|
}
|
|
|
|
var fieldname = comment.substring(0, equal).toLowerCase().replace(' ', '');
|
|
var propname = FLACFIELDS[fieldname];
|
|
if (propname) { // Do we care about this field?
|
|
var value = comment.substring(equal + 1);
|
|
if (INTFIELDS.indexOf(propname) !== -1) {
|
|
value = parseInt(value, 10);
|
|
}
|
|
return {field: propname, value: value};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
parse: parse
|
|
};
|
|
})();
|