VulpesOS/assets/webapps/music-oga.gaiamobile.org/js/metadata/ogg.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

205 lines
5.8 KiB
JavaScript

/* global LazyLoader, VorbisPictureComment */
/* exported OggMetadata */
'use strict';
/**
* Parse files with Ogg metadata (e.g. Vorbis comments or Opus tags).
*
* Format information:
* http://wiki.xiph.org/VorbisComment
*/
var OggMetadata = (function() {
// Fields that should be stored as integers, not strings
var INTFIELDS = [
'tracknum', 'trackcount', 'discnum', 'disccount'
];
// Map ogg field names to metadata property names
var OGGFIELDS = {
title: 'title',
artist: 'artist',
album: 'album',
tracknumber: 'tracknum',
tracktotal: 'trackcount',
discnumber: 'discnum',
disctotal: 'disccount',
metadata_block_picture: 'picture'
};
/**
* A type thrown if we walk off the end of an Ogg page.
*/
function EndOfPageError() {}
/**
* Parse a file and return a Promise with the metadata.
*
* @param {BlobView} blobview The audio file to parse.
* @param {Metadata} metadata The (partially filled-in) metadata object.
* @return {Promise} A Promise returning the parsed metadata object.
*/
function parse(blobview) {
readIdentificationHeader(blobview);
return readCommentHeader(blobview);
}
/**
* Read the identification header of an Ogg container. It's always the first
* page.
*
* @param {BlobView} page The audio file being parsed.
*/
function readIdentificationHeader(page) {
var header = readPageHeader(page);
if (header.segment_table.length !== 1) {
throw new Error(
'ogg identification header expected as only packet of first page'
);
}
// Skip over the identification header.
page.advance(header.segment_table[0]);
}
/**
* Read the comment header of an Ogg container.
*
* @param {BlobView} page The audio file being parsed.
* @return {Promise} A Promise that resolves with the completed metadata.
*/
function readCommentHeader(page) {
var metadata = {};
var header = readPageHeader(page);
var sum = function(a, b) { return a + b; };
var comment_length = Array.reduce(header.segment_table, sum, 0);
return new Promise((resolve, reject) => {
page.getMore(page.index, comment_length, (fullpage, err) => {
if (err) {
reject(err);
return;
}
try {
// Look for a comment header from a supported codec
var first_byte = fullpage.readByte();
var valid = false;
switch (first_byte) {
case 3:
valid = fullpage.readBinaryText(6) === 'vorbis';
metadata.tag_format = 'vorbis';
break;
case 79:
valid = fullpage.readBinaryText(7) === 'pusTags';
metadata.tag_format = 'opus';
break;
}
if (!valid) {
reject('malformed ogg comment packet');
return;
}
readAllComments(fullpage, metadata);
LazyLoader.load('js/metadata/vorbis_picture.js').then(() => {
return VorbisPictureComment.parsePictureComment(metadata);
}).then(resolve);
} catch(e) {
reject(e);
}
});
});
}
/**
* Read the header for an Ogg page.
*
* @param {BlobView} page The audio file being parsed.
* @return {Object} An object containing the page's segment table.
*/
function readPageHeader(page) {
var capture_pattern = page.readBinaryText(4);
if (capture_pattern !== 'OggS') {
throw new Error('malformed ogg page header');
}
// Skip over some header fields until we reach the page segments.
page.advance(22);
var page_segments = page.readUnsignedByte();
var segment_table = page.readUnsignedByteArray(page_segments);
return {
segment_table: segment_table
};
}
/**
* Read all the comments in an Ogg container.
*
* @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 libvorbis 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 if (comment.field !== 'picture') {
// We already have a value, so append this new one.
metadata[comment.field] += ' / ' + comment.value;
}
}
} catch (e) {
if (e instanceof EndOfPageError) {
return;
}
console.warn('Error parsing vorbis comment', e);
}
}
}
/**
* Read a single comment field.
*
* @param {BlobView} page The audio file being parsed.
*/
function readComment(page) {
if (page.remaining() < 4) { // 4 bytes for comment-length variable
// TODO: handle metadata that uses multiple pages
throw new EndOfPageError();
}
var comment_length = page.readUnsignedInt(true);
if (comment_length > page.remaining()) {
// TODO: handle metadata that uses multiple pages
throw new EndOfPageError();
}
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 = OGGFIELDS[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 {
parse: parse
};
})();