UNPKG

jsmediatags

Version:
601 lines (521 loc) 20.7 kB
'use strict'; function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } var MediaFileReader = require('./MediaFileReader'); var StringUtils = require('./StringUtils'); var ArrayFileReader = require('./ArrayFileReader'); var FRAME_DESCRIPTIONS = { // v2.2 "BUF": "Recommended buffer size", "CNT": "Play counter", "COM": "Comments", "CRA": "Audio encryption", "CRM": "Encrypted meta frame", "ETC": "Event timing codes", "EQU": "Equalization", "GEO": "General encapsulated object", "IPL": "Involved people list", "LNK": "Linked information", "MCI": "Music CD Identifier", "MLL": "MPEG location lookup table", "PIC": "Attached picture", "POP": "Popularimeter", "REV": "Reverb", "RVA": "Relative volume adjustment", "SLT": "Synchronized lyric/text", "STC": "Synced tempo codes", "TAL": "Album/Movie/Show title", "TBP": "BPM (Beats Per Minute)", "TCM": "Composer", "TCO": "Content type", "TCR": "Copyright message", "TDA": "Date", "TDY": "Playlist delay", "TEN": "Encoded by", "TFT": "File type", "TIM": "Time", "TKE": "Initial key", "TLA": "Language(s)", "TLE": "Length", "TMT": "Media type", "TOA": "Original artist(s)/performer(s)", "TOF": "Original filename", "TOL": "Original Lyricist(s)/text writer(s)", "TOR": "Original release year", "TOT": "Original album/Movie/Show title", "TP1": "Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group", "TP2": "Band/Orchestra/Accompaniment", "TP3": "Conductor/Performer refinement", "TP4": "Interpreted, remixed, or otherwise modified by", "TPA": "Part of a set", "TPB": "Publisher", "TRC": "ISRC (International Standard Recording Code)", "TRD": "Recording dates", "TRK": "Track number/Position in set", "TSI": "Size", "TSS": "Software/hardware and settings used for encoding", "TT1": "Content group description", "TT2": "Title/Songname/Content description", "TT3": "Subtitle/Description refinement", "TXT": "Lyricist/text writer", "TXX": "User defined text information frame", "TYE": "Year", "UFI": "Unique file identifier", "ULT": "Unsychronized lyric/text transcription", "WAF": "Official audio file webpage", "WAR": "Official artist/performer webpage", "WAS": "Official audio source webpage", "WCM": "Commercial information", "WCP": "Copyright/Legal information", "WPB": "Publishers official webpage", "WXX": "User defined URL link frame", // v2.3 "AENC": "Audio encryption", "APIC": "Attached picture", "ASPI": "Audio seek point index", "CHAP": "Chapter", "CTOC": "Table of contents", "COMM": "Comments", "COMR": "Commercial frame", "ENCR": "Encryption method registration", "EQU2": "Equalisation (2)", "EQUA": "Equalization", "ETCO": "Event timing codes", "GEOB": "General encapsulated object", "GRID": "Group identification registration", "IPLS": "Involved people list", "LINK": "Linked information", "MCDI": "Music CD identifier", "MLLT": "MPEG location lookup table", "OWNE": "Ownership frame", "PRIV": "Private frame", "PCNT": "Play counter", "POPM": "Popularimeter", "POSS": "Position synchronisation frame", "RBUF": "Recommended buffer size", "RVA2": "Relative volume adjustment (2)", "RVAD": "Relative volume adjustment", "RVRB": "Reverb", "SEEK": "Seek frame", "SYLT": "Synchronized lyric/text", "SYTC": "Synchronized tempo codes", "TALB": "Album/Movie/Show title", "TBPM": "BPM (beats per minute)", "TCOM": "Composer", "TCON": "Content type", "TCOP": "Copyright message", "TDAT": "Date", "TDLY": "Playlist delay", "TDRC": "Recording time", "TDRL": "Release time", "TDTG": "Tagging time", "TENC": "Encoded by", "TEXT": "Lyricist/Text writer", "TFLT": "File type", "TIME": "Time", "TIPL": "Involved people list", "TIT1": "Content group description", "TIT2": "Title/songname/content description", "TIT3": "Subtitle/Description refinement", "TKEY": "Initial key", "TLAN": "Language(s)", "TLEN": "Length", "TMCL": "Musician credits list", "TMED": "Media type", "TMOO": "Mood", "TOAL": "Original album/movie/show title", "TOFN": "Original filename", "TOLY": "Original lyricist(s)/text writer(s)", "TOPE": "Original artist(s)/performer(s)", "TORY": "Original release year", "TOWN": "File owner/licensee", "TPE1": "Lead performer(s)/Soloist(s)", "TPE2": "Band/orchestra/accompaniment", "TPE3": "Conductor/performer refinement", "TPE4": "Interpreted, remixed, or otherwise modified by", "TPOS": "Part of a set", "TPRO": "Produced notice", "TPUB": "Publisher", "TRCK": "Track number/Position in set", "TRDA": "Recording dates", "TRSN": "Internet radio station name", "TRSO": "Internet radio station owner", "TSOA": "Album sort order", "TSOP": "Performer sort order", "TSOT": "Title sort order", "TSIZ": "Size", "TSRC": "ISRC (international standard recording code)", "TSSE": "Software/Hardware and settings used for encoding", "TSST": "Set subtitle", "TYER": "Year", "TXXX": "User defined text information frame", "UFID": "Unique file identifier", "USER": "Terms of use", "USLT": "Unsychronized lyric/text transcription", "WCOM": "Commercial information", "WCOP": "Copyright/Legal information", "WOAF": "Official audio file webpage", "WOAR": "Official artist/performer webpage", "WOAS": "Official audio source webpage", "WORS": "Official internet radio station homepage", "WPAY": "Payment", "WPUB": "Publishers official webpage", "WXXX": "User defined URL link frame" }; var ID3v2FrameReader = /*#__PURE__*/function () { function ID3v2FrameReader() { _classCallCheck(this, ID3v2FrameReader); } _createClass(ID3v2FrameReader, null, [{ key: "getFrameReaderFunction", value: function getFrameReaderFunction(frameId) { if (frameId in frameReaderFunctions) { return frameReaderFunctions[frameId]; } else if (frameId[0] === "T") { // All frame ids starting with T are text tags. return frameReaderFunctions["T*"]; } else if (frameId[0] === "W") { // All frame ids starting with W are url tags. return frameReaderFunctions["W*"]; } else { return null; } } /** * All the frames consists of a frame header followed by one or more fields * containing the actual information. * The frame ID made out of the characters capital A-Z and 0-9. Identifiers * beginning with "X", "Y" and "Z" are for experimental use and free for * everyone to use, without the need to set the experimental bit in the tag * header. Have in mind that someone else might have used the same identifier * as you. All other identifiers are either used or reserved for future use. * The frame ID is followed by a size descriptor, making a total header size * of ten bytes in every frame. The size is calculated as frame size excluding * frame header (frame size - 10). */ }, { key: "readFrames", value: function readFrames(offset, end, data, id3header, tags) { var frames = {}; var frameHeaderSize = this._getFrameHeaderSize(id3header); // console.log('header', id3header); while ( // we should be able to read at least the frame header offset < end - frameHeaderSize) { var header = this._readFrameHeader(data, offset, id3header); var frameId = header.id; // No frame ID sometimes means it's the last frame (GTFO). if (!frameId) { break; } var flags = header.flags; var frameSize = header.size; var frameDataOffset = offset + header.headerSize; var frameData = data; // console.log(offset, frameId, header.size + header.headerSize, flags && flags.format.unsynchronisation); // advance data offset to the next frame data offset += header.headerSize + header.size; // skip unwanted tags if (tags && tags.indexOf(frameId) === -1) { continue; } // Workaround: MP3ext V3.3.17 places a non-compliant padding string at // the end of the ID3v2 header. A string like "MP3ext V3.3.19(ansi)" // is added multiple times at the end of the ID3 tag. More information // about this issue can be found at // https://github.com/aadsm/jsmediatags/issues/58#issuecomment-313865336 if (frameId === 'MP3e' || frameId === '\x00MP3' || frameId === '\x00\x00MP' || frameId === ' MP3') { break; } var unsyncData; if (flags && flags.format.unsynchronisation && !id3header.flags.unsynchronisation) { frameData = this.getUnsyncFileReader(frameData, frameDataOffset, frameSize); frameDataOffset = 0; frameSize = frameData.getSize(); } // the first 4 bytes are the real data size // (after unsynchronisation && encryption) if (flags && flags.format.data_length_indicator) { // var frameDataSize = frameData.getSynchsafeInteger32At(frameDataOffset); frameDataOffset += 4; frameSize -= 4; } var readFrameFunc = ID3v2FrameReader.getFrameReaderFunction(frameId); var parsedData = readFrameFunc ? readFrameFunc.apply(this, [frameDataOffset, frameSize, frameData, flags, id3header]) : null; var desc = this._getFrameDescription(frameId); var frame = { id: frameId, size: frameSize, description: desc, data: parsedData }; if (frameId in frames) { if (frames[frameId].id) { frames[frameId] = [frames[frameId]]; } frames[frameId].push(frame); } else { frames[frameId] = frame; } } return frames; } }, { key: "_getFrameHeaderSize", value: function _getFrameHeaderSize(id3header) { var major = id3header.major; if (major == 2) { return 6; } else if (major == 3 || major == 4) { return 10; } else { return 0; } } }, { key: "_readFrameHeader", value: function _readFrameHeader(data, offset, id3header) { var major = id3header.major; var flags = null; var frameHeaderSize = this._getFrameHeaderSize(id3header); switch (major) { case 2: var frameId = data.getStringAt(offset, 3); var frameSize = data.getInteger24At(offset + 3, true); break; case 3: var frameId = data.getStringAt(offset, 4); var frameSize = data.getLongAt(offset + 4, true); break; case 4: var frameId = data.getStringAt(offset, 4); var frameSize = data.getSynchsafeInteger32At(offset + 4); break; } if (frameId == String.fromCharCode(0, 0, 0) || frameId == String.fromCharCode(0, 0, 0, 0)) { frameId = ""; } // if frameId is empty then it's the last frame if (frameId) { // read frame message and format flags if (major > 2) { flags = this._readFrameFlags(data, offset + 8); } } return { "id": frameId || "", "size": frameSize || 0, "headerSize": frameHeaderSize || 0, "flags": flags }; } }, { key: "_readFrameFlags", value: function _readFrameFlags(data, offset) { return { message: { tag_alter_preservation: data.isBitSetAt(offset, 6), file_alter_preservation: data.isBitSetAt(offset, 5), read_only: data.isBitSetAt(offset, 4) }, format: { grouping_identity: data.isBitSetAt(offset + 1, 7), compression: data.isBitSetAt(offset + 1, 3), encryption: data.isBitSetAt(offset + 1, 2), unsynchronisation: data.isBitSetAt(offset + 1, 1), data_length_indicator: data.isBitSetAt(offset + 1, 0) } }; } }, { key: "_getFrameDescription", value: function _getFrameDescription(frameId) { if (frameId in FRAME_DESCRIPTIONS) { return FRAME_DESCRIPTIONS[frameId]; } else { return 'Unknown'; } } }, { key: "getUnsyncFileReader", value: function getUnsyncFileReader(data, offset, size) { var frameData = data.getBytesAt(offset, size); for (var i = 0; i < frameData.length - 1; i++) { if (frameData[i] === 0xff && frameData[i + 1] === 0x00) { frameData.splice(i + 1, 1); } } return new ArrayFileReader(frameData); } }]); return ID3v2FrameReader; }(); ; var frameReaderFunctions = {}; frameReaderFunctions['APIC'] = function readPictureFrame(offset, length, data, flags, id3header) { var start = offset; var charset = getTextEncoding(data.getByteAt(offset)); switch (id3header && id3header.major) { case 2: var format = data.getStringAt(offset + 1, 3); offset += 4; break; case 3: case 4: var format = data.getStringWithCharsetAt(offset + 1, length - 1); offset += 1 + format.bytesReadCount; break; default: throw new Error("Couldn't read ID3v2 major version."); } var bite = data.getByteAt(offset); var type = PICTURE_TYPE[bite]; var desc = data.getStringWithCharsetAt(offset + 1, length - (offset - start) - 1, charset); offset += 1 + desc.bytesReadCount; return { "format": format.toString(), "type": type, "description": desc.toString(), "data": data.getBytesAt(offset, start + length - offset) }; }; // ID3v2 chapters according to http://id3.org/id3v2-chapters-1.0 frameReaderFunctions['CHAP'] = function readChapterFrame(offset, length, data, flags, id3header) { var originalOffset = offset; var result = {}; var id = StringUtils.readNullTerminatedString(data.getBytesAt(offset, length)); result.id = id.toString(); offset += id.bytesReadCount; result.startTime = data.getLongAt(offset, true); offset += 4; result.endTime = data.getLongAt(offset, true); offset += 4; result.startOffset = data.getLongAt(offset, true); offset += 4; result.endOffset = data.getLongAt(offset, true); offset += 4; var remainingLength = length - (offset - originalOffset); result.subFrames = this.readFrames(offset, offset + remainingLength, data, id3header); return result; }; // ID3v2 table of contents according to http://id3.org/id3v2-chapters-1.0 frameReaderFunctions['CTOC'] = function readTableOfContentsFrame(offset, length, data, flags, id3header) { var originalOffset = offset; var result = { childElementIds: [], id: undefined, topLevel: undefined, ordered: undefined, entryCount: undefined, subFrames: undefined }; var id = StringUtils.readNullTerminatedString(data.getBytesAt(offset, length)); result.id = id.toString(); offset += id.bytesReadCount; result.topLevel = data.isBitSetAt(offset, 1); result.ordered = data.isBitSetAt(offset, 0); offset++; result.entryCount = data.getByteAt(offset); offset++; for (var i = 0; i < result.entryCount; i++) { var childId = StringUtils.readNullTerminatedString(data.getBytesAt(offset, length - (offset - originalOffset))); result.childElementIds.push(childId.toString()); offset += childId.bytesReadCount; } var remainingLength = length - (offset - originalOffset); result.subFrames = this.readFrames(offset, offset + remainingLength, data, id3header); return result; }; frameReaderFunctions['COMM'] = function readCommentsFrame(offset, length, data, flags, id3header) { var start = offset; var charset = getTextEncoding(data.getByteAt(offset)); var language = data.getStringAt(offset + 1, 3); var shortdesc = data.getStringWithCharsetAt(offset + 4, length - 4, charset); offset += 4 + shortdesc.bytesReadCount; var text = data.getStringWithCharsetAt(offset, start + length - offset, charset); return { language: language, short_description: shortdesc.toString(), text: text.toString() }; }; frameReaderFunctions['COM'] = frameReaderFunctions['COMM']; frameReaderFunctions['PIC'] = function (offset, length, data, flags, id3header) { return frameReaderFunctions['APIC'](offset, length, data, flags, id3header); }; frameReaderFunctions['PCNT'] = function readCounterFrame(offset, length, data, flags, id3header) { // FIXME: implement the rest of the spec return data.getLongAt(offset, false); }; frameReaderFunctions['CNT'] = frameReaderFunctions['PCNT']; frameReaderFunctions['T*'] = function readTextFrame(offset, length, data, flags, id3header) { var charset = getTextEncoding(data.getByteAt(offset)); return data.getStringWithCharsetAt(offset + 1, length - 1, charset).toString(); }; frameReaderFunctions['TXXX'] = function readTextFrame(offset, length, data, flags, id3header) { var charset = getTextEncoding(data.getByteAt(offset)); return getUserDefinedFields(offset, length, data, charset); }; frameReaderFunctions['WXXX'] = function readUrlFrame(offset, length, data, flags, id3header) { if (length === 0) { return null; } var charset = getTextEncoding(data.getByteAt(offset)); return getUserDefinedFields(offset, length, data, charset); }; frameReaderFunctions['W*'] = function readUrlFrame(offset, length, data, flags, id3header) { if (length === 0) { return null; } return data.getStringWithCharsetAt(offset, length, 'iso-8859-1').toString(); }; frameReaderFunctions['TCON'] = function readGenreFrame(offset, length, data, flags) { var text = frameReaderFunctions['T*'].apply(this, arguments); return text.replace(/^\(\d+\)/, ''); }; frameReaderFunctions['TCO'] = frameReaderFunctions['TCON']; frameReaderFunctions['USLT'] = function readLyricsFrame(offset, length, data, flags, id3header) { var start = offset; var charset = getTextEncoding(data.getByteAt(offset)); var language = data.getStringAt(offset + 1, 3); var descriptor = data.getStringWithCharsetAt(offset + 4, length - 4, charset); offset += 4 + descriptor.bytesReadCount; var lyrics = data.getStringWithCharsetAt(offset, start + length - offset, charset); return { language: language, descriptor: descriptor.toString(), lyrics: lyrics.toString() }; }; frameReaderFunctions['ULT'] = frameReaderFunctions['USLT']; frameReaderFunctions['UFID'] = function readLyricsFrame(offset, length, data, flags, id3header) { var ownerIdentifier = StringUtils.readNullTerminatedString(data.getBytesAt(offset, length)); offset += ownerIdentifier.bytesReadCount; var identifier = data.getBytesAt(offset, length - ownerIdentifier.bytesReadCount); return { ownerIdentifier: ownerIdentifier.toString(), identifier: identifier }; }; function getTextEncoding(bite) { var charset; switch (bite) { case 0x00: charset = 'iso-8859-1'; break; case 0x01: charset = 'utf-16'; break; case 0x02: charset = 'utf-16be'; break; case 0x03: charset = 'utf-8'; break; default: charset = 'iso-8859-1'; } return charset; } // Handles reading description/data from either http://id3.org/id3v2.3.0#User_defined_text_information_frame // and http://id3.org/id3v2.3.0#User_defined_URL_link_frame function getUserDefinedFields(offset, length, data, charset) { var userDesc = data.getStringWithCharsetAt(offset + 1, length - 1, charset); var userDefinedData = data.getStringWithCharsetAt(offset + 1 + userDesc.bytesReadCount, length - 1 - userDesc.bytesReadCount, charset); return { user_description: userDesc.toString(), data: userDefinedData.toString() }; } var PICTURE_TYPE = ["Other", "32x32 pixels 'file icon' (PNG only)", "Other file icon", "Cover (front)", "Cover (back)", "Leaflet page", "Media (e.g. label side of CD)", "Lead artist/lead performer/soloist", "Artist/performer", "Conductor", "Band/Orchestra", "Composer", "Lyricist/text writer", "Recording Location", "During recording", "During performance", "Movie/video screen capture", "A bright coloured fish", "Illustration", "Band/artist logotype", "Publisher/Studio logotype"]; module.exports = ID3v2FrameReader;