您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows you to edit metadata of FLAC files. CO
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://updategreasyfork.deno.dev/scripts/40545/646530/JS%20FLACMetadataEditor.js
/* jslint esversion: 6, bitwise: true */ // ==UserScript== // @name JS FLACMetadataEditor // @description Allows you to edit metadata of FLAC files. CO // @namespace universe.earth.www.ahohnmyc // @version 0.0.2.1 // @license GPL-3.0-or-later // @grant none // ==/UserScript== const FLACMetadataEditor = (()=>{ 'use strict'; const _version = '0.0.2.1'; class VorbisComment extends Array {} class VorbisCommentPacket { /* Need to easy initialization */ _addComment(field) { const value = field.split('=')[1]; field = field.split('=')[0].toUpperCase(); if (!this.hasOwnProperty(field)) this[field] = new VorbisComment(); if (!this[field].some(storedValue=> storedValue===value)) this[field].push(value.toString()); return this; } toStringArray() { const array = []; Object.keys(this).sort().forEach(key=> { this[key].forEach(value=> { array.push(key+'='+value); }); }); return array; } } class FLACMetadataBlockData {} class FLACMetadataBlock { constructor() { this.blockType = ''; this.blockTypeNubmer = 0; this.blockSize = 0; this.data = new FLACMetadataBlockData(); this.offset = 0; } get serializedSize() { switch (this.blockType) { case 'STREAMINFO': return 34; case 'PADDING': return this.blockSize; case 'APPLICATION': return 4+this.data.applicationData.length; case 'SEEKTABLE': return this.data.points.length*18; case 'VORBIS_COMMENT': const totl = this.data.comments.toStringArray().reduce((sum, str)=>sum+4+str.toUTF8().length, 0); return 4+this.data.vendorString.length+4+ totl; case 'CUESHEET': return 0; case 'PICTURE': return 4+4+this.data.MIMEType.toUTF8().length+4+this.data.description.toUTF8().length+4+4+4+4+4+this.data.data.length; } } } class FLACMetadataBlocks extends Array {} class FLACMetadata { constructor() { this.blocks = new FLACMetadataBlocks(); this.framesOffset = 0; this.signature = ''; } } class _FLACMetadataEditor { get scriptVersion() {return _version;} constructor(buffer) { if (!buffer || typeof buffer !== 'object' || !('byteLength' in buffer)) { throw new Error('First argument should be an instance of ArrayBuffer or Buffer'); } this.arrayBuffer = buffer; this.metadata = new FLACMetadata(); String.prototype.toUTF8 = function(str = null) { return new TextEncoder().encode(str ? str : this); }; this._parseMetadata(); return this; } /* unpack */ _getBytesAsNumber (array, start=0, end=array.length-start) {return Array.from(array.subarray(start, start+end)).reduce ((result, b)=>result=256*result+b, 0);} _getBytesAsNumberLittleEndian(array, start=0, end=array.length-start) {return Array.from(array.subarray(start, start+end)).reduceRight((result, b)=>result=256*result+b, 0);} _getBytesAsHexString (array, start=0, end=array.length-start) {return Array.from(array.subarray(start, start+end)).map(n=>(n>>4).toString(16)+(n&0xF).toString(16)).join('');} _getBytesAsUTF8String(array, start=0, end=array.length-start) {return new TextDecoder().decode(array.subarray(start, start+end));} _getBlockType(number){ switch (number) { case 0: return 'STREAMINFO'; case 1: return 'PADDING'; case 2: return 'APPLICATION'; case 3: return 'SEEKTABLE'; case 4: return 'VORBIS_COMMENT'; case 5: return 'CUESHEET'; case 6: return 'PICTURE'; case 127: return 'invalid, to avoid confusion with a frame sync code'; default: return 'reserved'; } } /* pack */ _uint32ToUint8Array(uint32) { const eightBitMask = 0xff; return [ (uint32 >>> 24) & eightBitMask, (uint32 >>> 16) & eightBitMask, (uint32 >>> 8) & eightBitMask, uint32 & eightBitMask, ]; } _uint24ToUint8Array(uint32) { const eightBitMask = 0xff; return [ (uint32 >>> 16) & eightBitMask, (uint32 >>> 8) & eightBitMask, uint32 & eightBitMask, ]; } _uint16ToUint8Array(uint32) { const eightBitMask = 0xff; return [ (uint32 >>> 8) & eightBitMask, uint32 & eightBitMask, ]; } _hexStringToUint8Array(str) { return str.replace(/(\w\w)/g,'$1,').slice(0,-1).split(',').map(s=> (parseInt(s[0],16)<<4) + parseInt(s[1],16)); } get _vorbisComment() { const block = this.metadata.blocks.find(block=>block.blockType==='VORBIS_COMMENT'); if (block) return block.data; } addComment(field, value = null) { if (field) { if (!value) { const splitted = field.split('='); if (!splitted[1]) return this; value = splitted[1]; field = splitted[0]; } field = field.toUpperCase(); if (!this._vorbisComment.comments.hasOwnProperty(field)) this._vorbisComment.comments[field] = new VorbisComment(); if (!this._vorbisComment.comments[field].find(storedValue=> storedValue===value)) this._vorbisComment.comments[field].push(value.toString()); } return this; } removeComment(field = null, value = null) { if (!field) { Object.keys(this._vorbisComment.comments).forEach(key=> delete this._vorbisComment.comments[key]); } else { field = field.toUpperCase(); if (!value) { delete this._vorbisComment.comments[field]; } else { value = value.toString(); if (this.hasOwnProperty(field)) this._vorbisComment.comments[field] = this._vorbisComment.comments[field].filter(storedValue=> storedValue!==value); } } return this; } getComment(field) { return this._vorbisComment.comments[field.toUpperCase()]; } addPicture(dataInput) { if (!dataInput.data || !dataInput.data || typeof dataInput.data !== 'object' || !('byteLength' in dataInput.data)) { throw new Error('Field "data" should be an instance of ArrayBuffer or Buffer'); } dataInput.data = new Uint8Array(dataInput.data); const dataDefault = { APICtype: 3, MIMEType: 'image/jpeg', colorDepth: 0, colorNumber: 0, data: new Uint8Array([]), description: '', width: 0, height: 0, }; const block = new FLACMetadataBlock(); block.blockTypeNubmer = 6; block.blockType = 'PICTURE'; for (let property in dataDefault) { if (dataInput[property]) { block.data[property] = dataInput[property]; } else { block.data[property] = dataDefault[property]; } } const bl = this.metadata.blocks; let index = bl.length; if (bl[bl.length-1].blockType === 'PADDING') index--; bl.splice(index, 0, block); this.metadata.blocks = bl; return this; } _serializeMetadataBlock(block) { const bytes = new Uint8Array(block.serializedSize); const data = block.data; let offset = 0; switch (block.blockType) { case 'STREAMINFO': bytes.set(this._uint16ToUint8Array(data.minBlockSize)); offset += 2; bytes.set(this._uint16ToUint8Array(data.maxBlockSize), offset); offset += 2; bytes.set(this._uint24ToUint8Array(data.minFrameSize), offset); offset += 3; bytes.set(this._uint24ToUint8Array(data.maxFrameSize), offset); offset += 3; bytes.set(this._uint24ToUint8Array((data.sampleRate<<4) + (data.numberOfChannels-1<<1) + (data.bitsPerSample-1>>4)), offset); offset += 3; bytes[offset] = ((data.bitsPerSample-1&0xF)<<4) + (Math.trunc(data.totalSamples/Math.pow(2,32))&0xF); offset += 1; bytes.set(this._uint32ToUint8Array(data.totalSamples), offset); offset += 4; bytes.set(this._hexStringToUint8Array(data.rawMD5), offset); break; case 'PADDING': break; case 'APPLICATION': bytes.set(data.applicationID.toUTF8()); offset += 4; bytes.set(data.applicationData, offset); break; case 'SEEKTABLE': data.points.forEach(point=> { bytes.set(this._hexStringToUint8Array(point.sampleNumber), offset); bytes.set(this._hexStringToUint8Array(point.offset), offset+8); bytes.set(this._hexStringToUint8Array(point.numberOfSamples), offset+16); offset += 18; }); break; case 'VORBIS_COMMENT': bytes.set(this._uint32ToUint8Array(data.vendorString.toUTF8().length).reverse(), offset); offset += 4; bytes.set(data.vendorString.toUTF8(), offset); offset += data.vendorString.toUTF8().length; const comments = data.comments.toStringArray(); bytes.set(this._uint32ToUint8Array(comments.length).reverse(), offset); offset += 4; comments.forEach(comment=> { bytes.set(this._uint32ToUint8Array(comment.toUTF8().length).reverse(), offset); offset += 4; bytes.set(comment.toUTF8(), offset); offset += comment.toUTF8().length; }); break; case 'CUESHEET': break; case 'PICTURE': bytes.set(this._uint32ToUint8Array(data.APICtype)); offset += 4; bytes.set(this._uint32ToUint8Array(data.MIMEType.toUTF8().length), offset); offset += 4; bytes.set(data.MIMEType.toUTF8(), offset); offset += data.MIMEType.toUTF8().length; bytes.set(this._uint32ToUint8Array(data.description.toUTF8().length), offset); offset += 4; bytes.set(data.description.toUTF8(), offset); offset += data.description.toUTF8().length; bytes.set(this._uint32ToUint8Array(data.width), offset); offset += 4; bytes.set(this._uint32ToUint8Array(data.height), offset); offset += 4; bytes.set(this._uint32ToUint8Array(data.colorDepth), offset); offset += 4; bytes.set(this._uint32ToUint8Array(data.colorNumber), offset); offset += 4; bytes.set(this._uint32ToUint8Array(data.data.length), offset); offset += 4; bytes.set(data.data, offset); break; } return bytes; } serializeMetadata() { const newMetadataLengthFull = 4+this.metadata.blocks.reduce((sum, block)=>sum+4+block.serializedSize, 0); const newSize = newMetadataLengthFull + (this.arrayBuffer.byteLength>this.metadata.framesOffset ? this.arrayBuffer.byteLength-this.metadata.framesOffset : 0); const bytes = new Uint8Array(newSize); bytes.set(this.metadata.signature.toUTF8()); let offset = 4; let lastBlock = false; this.metadata.blocks.forEach((block, n, blocks)=>{ if (blocks.length-1 === n) lastBlock = true; bytes[offset] = block.blockTypeNubmer | (lastBlock<<7); offset += 1; bytes.set(this._uint24ToUint8Array(block.serializedSize), offset); offset += 3; bytes.set(this._serializeMetadataBlock(block), offset); offset += block.serializedSize; }); // console.info('old meta size: %d, new: %d, delta: %d', this.metadata.framesOffset, newMetadataLengthFull, Math.abs(this.metadata.framesOffset-newMetadataLengthFull) ); // console.info('old size: %d, new: %d, delta: %d', this.arrayBuffer.byteLength, newSize, Math.abs(this.arrayBuffer.byteLength-newSize) ); // console.info('frames size: %d, to copy: %d', this.arrayBuffer.byteLength-this.metadata.framesOffset, new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset).length); // console.info('offset: %d', offset ); bytes.set(new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset), offset); this.arrayBuffer = bytes.buffer; return this; } _parseMetadataBlock(array, arrayOffset, type, size) { const blockData = array.subarray(arrayOffset, arrayOffset+size); let offset = 0; const data = new FLACMetadataBlockData(); switch (type) { case 'STREAMINFO': data.minBlockSize = this._getBytesAsNumber(blockData, offset, 2); offset += 2; data.maxBlockSize = this._getBytesAsNumber(blockData, offset, 2); offset += 2; data.minFrameSize = this._getBytesAsNumber(blockData, offset, 3); offset += 3; data.maxFrameSize = this._getBytesAsNumber(blockData, offset, 3); offset += 3; data.sampleRate = this._getBytesAsNumber(blockData, offset, 3)>>4; offset += 2; data.numberOfChannels = 1+ ((blockData[offset]>>1) &7); data.bitsPerSample = 1+ ((1&blockData[offset]) <<4) + (blockData[offset+1]>>4); offset += 1; data.totalSamples = (blockData[offset]&0xF)*Math.pow(2,32) + this._getBytesAsNumber(blockData, offset+1, 4); offset += 5; data.rawMD5 = this._getBytesAsHexString(blockData, offset, 16).toUpperCase(); break; case 'PADDING': break; case 'APPLICATION': data.applicationID = this._getBytesAsUTF8String(blockData, offset, 4); offset += 4; data.applicationData = blockData.subarray(offset); break; case 'SEEKTABLE': data.pointCount = size/18; data.points = []; for (let i=0; i<data.pointCount; i++) { data.points.push({ sampleNumber: this._getBytesAsHexString(blockData, offset, 8), offset: this._getBytesAsHexString(blockData, offset+8, 8), numberOfSamples: this._getBytesAsHexString(blockData, offset+16, 2), }); offset += 18; } break; case 'VORBIS_COMMENT': const vendorLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4); offset += 4; data.vendorString = this._getBytesAsUTF8String(blockData, offset, vendorLength); offset += vendorLength; const userCommentListLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4); offset += 4; data.comments = new VorbisCommentPacket(); let commentLength = 0; for (let i=0; i<userCommentListLength; i++) { commentLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4); offset += 4; data.comments._addComment(this._getBytesAsUTF8String(blockData, offset, commentLength)); offset += commentLength; } break; case 'CUESHEET': break; case 'PICTURE': data.APICtype = this._getBytesAsNumber(blockData, offset, 4); offset += 4; const MIMELength = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.MIMEType = this._getBytesAsUTF8String(blockData, offset, MIMELength); offset += MIMELength; const descriptionLength = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.description = this._getBytesAsUTF8String(blockData, offset, descriptionLength); offset += descriptionLength; data.width = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.height = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.colorDepth = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.colorNumber = this._getBytesAsNumber(blockData, offset, 4); offset += 4; const binarySize = this._getBytesAsNumber(blockData, offset, 4); offset += 4; data.data = blockData.subarray(offset, offset+binarySize); break; } return data; } _parseMetadata() { const bytes = new Uint8Array(this.arrayBuffer); this.metadata.signature = this._getBytesAsUTF8String(bytes,0,4); let offset = 4; let lastBlock = false; let block; let iteration = 0; while (!lastBlock && offset < bytes.length) { if (iteration++ > 42) throw new RangeError('Too much METADATA_BLOCKS. Looks like file corrupted'); block = new FLACMetadataBlock(); block.offset = offset; lastBlock = !!(bytes[offset] >> 7); block.blockTypeNubmer = bytes[offset] & 127; block.blockType = this._getBlockType(block.blockTypeNubmer); offset += 1; block.blockSize = this._getBytesAsNumber(bytes, offset, 3); offset += 3; block.data = this._parseMetadataBlock(bytes, offset, block.blockType, block.blockSize); offset += block.blockSize; // if (block.blockType !== 'PADDING') this.metadata.blocks.push(block); } this.metadata.framesOffset = offset; return this; } } return _FLACMetadataEditor; })();