您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A pure JavaScript implementation of the metaflac (the official FLAC tool written in C++) (The userscript port for https://github.com/ishowshao/metaflac-js/tree/master)
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/482520/1298549/metaflacjs.js
/* eslint-disable no-multi-spaces */ /* eslint-disable no-return-assign */ // ==UserScript== // @name metaflac.js // @namespace https://github.com/ishowshao/metaflac-js/ // @version 0.1.4.1 // @description A pure JavaScript implementation of the metaflac (the official FLAC tool written in C++) (The userscript port for https://github.com/ishowshao/metaflac-js/tree/master) // @author ishowshao, PY-DNG // @license https://github.com/ishowshao/metaflac-js/ // ==/UserScript== /* global BufferExport */ // !IMPORTANT! THIS USERSCRIPT LIBRARY REQUIRES https://gf.qytechs.cn/scripts/482519-buffer TO WORK! let Metaflac = await (async function __MAIN__() { 'use strict'; const Buffer = BufferExport.Buffer; const FileType = await import('https://fastly.jsdelivr.net/npm/[email protected]/+esm'); const formatVorbisComment = (function() { return (vendorString, commentList) => { const bufferArray = []; const vendorStringBuffer = Buffer.from(vendorString, 'utf8'); const vendorLengthBuffer = Buffer.alloc(4); vendorLengthBuffer.writeUInt32LE(vendorStringBuffer.length); const userCommentListLengthBuffer = Buffer.alloc(4); userCommentListLengthBuffer.writeUInt32LE(commentList.length); bufferArray.push(vendorLengthBuffer, vendorStringBuffer, userCommentListLengthBuffer); for (let i = 0; i < commentList.length; i++) { const comment = commentList[i]; const commentBuffer = Buffer.from(comment, 'utf8'); const lengthBuffer = Buffer.alloc(4); lengthBuffer.writeUInt32LE(commentBuffer.length); bufferArray.push(lengthBuffer, commentBuffer); } return Buffer.concat(bufferArray); } })(); const BLOCK_TYPE = { 0: 'STREAMINFO', 1: 'PADDING', 2: 'APPLICATION', 3: 'SEEKTABLE', 4: 'VORBIS_COMMENT', // There may be only one VORBIS_COMMENT block in a stream. 5: 'CUESHEET', 6: 'PICTURE', }; const STREAMINFO = 0; const PADDING = 1; const APPLICATION = 2; const SEEKTABLE = 3; const VORBIS_COMMENT = 4; const CUESHEET = 5; const PICTURE = 6; class Metaflac { constructor(flac) { if (typeof flac !== 'string' && typeof flac !== 'string' && !Buffer.isBuffer(flac)) { throw new Error('Metaflac(flac) flac must be string or buffer.'); } this.flac = flac; this.buffer = null; this.marker = ''; this.streamInfo = null; this.blocks = []; this.padding = null; this.vorbisComment = null; this.vendorString = ''; this.tags = []; this.pictures = []; this.picturesSpecs = []; this.picturesDatas = []; this.framesOffset = 0; this.init(); } async init() { typeof this.flac === 'string' ? this.buffer = await fetchArrayBuffer(this.flac) : this.buffer = this.flac; let offset = 0; const marker = this.buffer.slice(0, offset += 4).toString('ascii'); if (marker !== 'fLaC') { throw new Error('The file does not appear to be a FLAC file.'); } let blockType = 0; let isLastBlock = false; while (!isLastBlock) { blockType = this.buffer.readUInt8(offset++); isLastBlock = blockType > 128; blockType = blockType % 128; // console.log('Block Type: %d %s', blockType, BLOCK_TYPE[blockType]); const blockLength = this.buffer.readUIntBE(offset, 3); offset += 3; if (blockType === STREAMINFO) { this.streamInfo = this.buffer.slice(offset, offset + blockLength); } if (blockType === PADDING) { this.padding = this.buffer.slice(offset, offset + blockLength); } if (blockType === VORBIS_COMMENT) { this.vorbisComment = this.buffer.slice(offset, offset + blockLength); this.parseVorbisComment(); } if (blockType === PICTURE) { this.pictures.push(this.buffer.slice(offset, offset + blockLength)); this.parsePictureBlock(); } if ([APPLICATION, SEEKTABLE, CUESHEET].includes(blockType)) { this.blocks.push([blockType, this.buffer.slice(offset, offset + blockLength)]); } // console.log('Block Length: %d', blockLength); offset += blockLength; } this.framesOffset = offset; } parseVorbisComment() { const vendorLength = this.vorbisComment.readUInt32LE(0); // console.log('Vendor length: %d', vendorLength); this.vendorString = this.vorbisComment.slice(4, vendorLength + 4).toString('utf8'); // console.log('Vendor string: %s', this.vendorString); const userCommentListLength = this.vorbisComment.readUInt32LE(4 + vendorLength); // console.log('user_comment_list_length: %d', userCommentListLength); const userCommentListBuffer = this.vorbisComment.slice(4 + vendorLength + 4); for (let offset = 0; offset < userCommentListBuffer.length; ) { const length = userCommentListBuffer.readUInt32LE(offset); offset += 4; const comment = userCommentListBuffer.slice(offset, offset += length).toString('utf8'); // console.log('Comment length: %d, this.buffer: %s', length, comment); this.tags.push(comment); } } parsePictureBlock() { this.pictures.forEach(picture => { let offset = 0; const type = picture.readUInt32BE(offset); offset += 4; const mimeTypeLength = picture.readUInt32BE(offset); offset += 4; const mime = picture.slice(offset, offset + mimeTypeLength).toString('ascii'); offset += mimeTypeLength; const descriptionLength = picture.readUInt32BE(offset); offset += 4; const description = picture.slice(offset, offset += descriptionLength).toString('utf8'); const width = picture.readUInt32BE(offset); offset += 4; const height = picture.readUInt32BE(offset); offset += 4; const depth = picture.readUInt32BE(offset); offset += 4; const colors = picture.readUInt32BE(offset); offset += 4; const pictureDataLength = picture.readUInt32BE(offset); offset += 4; this.picturesDatas.push(picture.slice(offset, offset + pictureDataLength)); this.picturesSpecs.push(this.buildSpecification({ type, mime, description, width, height, depth, colors })); }); } getPicturesSpecs() { return this.picturesSpecs; } /** * Get the MD5 signature from the STREAMINFO block. */ getMd5sum() { return this.streamInfo.slice(18, 34).toString('hex'); } /** * Get the minimum block size from the STREAMINFO block. */ getMinBlocksize() { return this.streamInfo.readUInt16BE(0); } /** * Get the maximum block size from the STREAMINFO block. */ getMaxBlocksize() { return this.streamInfo.readUInt16BE(2); } /** * Get the minimum frame size from the STREAMINFO block. */ getMinFramesize() { return this.streamInfo.readUIntBE(4, 3); } /** * Get the maximum frame size from the STREAMINFO block. */ getMaxFramesize() { return this.streamInfo.readUIntBE(7, 3); } /** * Get the sample rate from the STREAMINFO block. */ getSampleRate() { // 20 bits number return this.streamInfo.readUIntBE(10, 3) >> 4; } /** * Get the number of channels from the STREAMINFO block. */ getChannels() { // 3 bits return this.streamInfo.readUIntBE(10, 3) & 0x00000f >> 1; } /** * Get the # of bits per sample from the STREAMINFO block. */ getBps() { return this.streamInfo.readUIntBE(12, 2) & 0x01f0 >> 4; } /** * Get the total # of samples from the STREAMINFO block. */ getTotalSamples() { return this.streamInfo.readUIntBE(13, 5) & 0x0fffffffff; } /** * Show the vendor string from the VORBIS_COMMENT block. */ getVendorTag() { return this.vendorString; } /** * Get all tags where the the field name matches NAME. * * @param {string} name */ getTag(name) { return this.tags.filter(item => { const itemName = item.split('=')[0]; return itemName === name; }).join('\n'); } /** * Remove all tags whose field name is NAME. * * @param {string} name */ removeTag(name) { this.tags = this.tags.filter(item => { const itemName = item.split('=')[0]; return itemName !== name; }); } /** * Remove first tag whose field name is NAME. * * @param {string} name */ removeFirstTag(name) { const found = this.tags.findIndex(item => { return item.split('=')[0] === name; }); if (found !== -1) { this.tags.splice(found, 1); } } /** * Remove all tags, leaving only the vendor string. */ removeAllTags() { this.tags = []; } /** * Add a tag. * The FIELD must comply with the Vorbis comment spec, of the form NAME=VALUE. If there is currently no tag block, one will be created. * * @param {string} field */ setTag(field) { if (field.indexOf('=') === -1) { throw new Error(`malformed vorbis comment field "${field}", field contains no '=' character`); } this.tags.push(field); } /** * Like setTag, except the VALUE is a filename whose contents will be read verbatim to set the tag value. * * @param {string} field */ async setTagFromFile(field) { const position = field.indexOf('='); if (position === -1) { throw new Error(`malformed vorbis comment field "${field}", field contains no '=' character`); } const name = field.substring(0, position); const filename = field.substr(position + 1); let value; try { value = await readAsText(filename, 'utf8'); } catch (e) { throw new Error(`can't open file '${filename}' for '${name}' tag value`); } this.tags.push(`${name}=${value}`); } /** * Import tags from a file. * Each line should be of the form NAME=VALUE. * * @param {string} filename */ async importTagsFrom(filename) { const tags = await readAsText(filename, 'utf8').split('\n'); tags.forEach(line => { if (line.indexOf('=') === -1) { throw new Error(`malformed vorbis comment "${line}", contains no '=' character`); } }); this.tags = this.tags.concat(tags); } /** * Export tags to a file. * Each line will be of the form NAME=VALUE. * * @param {string} filename */ exportTagsTo(filename) { dlText(filename, this.tags.join('\n'), 'utf8'); } /** * Import a picture and store it in a PICTURE metadata block. * * @param {string} filename */ async importPictureFrom(filename) { const picture = await fetchArrayBuffer(filename); const {mime} = await FileType.fileTypeFromBuffer(picture); if (mime !== 'image/jpeg' && mime !== 'image/png') { throw new Error(`only support image/jpeg and image/png picture temporarily, current import ${mime}`); } const dimensions = await imageSize(filename); const spec = this.buildSpecification({ mime: mime, width: dimensions.width, height: dimensions.height, }); this.pictures.push(this.buildPictureBlock(picture, spec)); this.picturesSpecs.push(spec); } /** * Import a picture and store it in a PICTURE metadata block. * * @param {Buffer} picture */ async importPictureFromBuffer(picture) { const {mime} = await FileType.fileTypeFromBuffer(picture); if (mime !== 'image/jpeg' && mime !== 'image/png') { throw new Error(`only support image/jpeg and image/png picture temporarily, current import ${mime}`); } const dimensions = await imageSize(picture); const spec = this.buildSpecification({ mime: mime, width: dimensions.width, height: dimensions.height, }); this.pictures.push(this.buildPictureBlock(picture, spec)); this.picturesSpecs.push(spec); } /** * Export PICTURE block to a file. * * @param {string} filename */ exportPictureTo(filename) { if (this.picturesDatas.length > 0) { dlText(filename, this.picturesDatas[0]); } } /** * Return all tags. */ getAllTags() { return this.tags; } buildSpecification(spec = {}) { const defaults = { type: 3, mime: 'image/jpeg', description: '', width: 0, height: 0, depth: 24, colors: 0, }; return Object.assign(defaults, spec); } /** * Build a picture block. * * @param {Buffer} picture * @param {Object} specification * @returns {Buffer} */ buildPictureBlock(picture, specification = {}) { const pictureType = Buffer.alloc(4); const mimeLength = Buffer.alloc(4); const mime = Buffer.from(specification.mime, 'ascii'); const descriptionLength = Buffer.alloc(4); const description = Buffer.from(specification.description, 'utf8'); const width = Buffer.alloc(4); const height = Buffer.alloc(4); const depth = Buffer.alloc(4); const colors = Buffer.alloc(4); const pictureLength = Buffer.alloc(4); pictureType.writeUInt32BE(specification.type); mimeLength.writeUInt32BE(specification.mime.length); descriptionLength.writeUInt32BE(specification.description.length); width.writeUInt32BE(specification.width); height.writeUInt32BE(specification.height); depth.writeUInt32BE(specification.depth); colors.writeUInt32BE(specification.colors); pictureLength.writeUInt32BE(picture.length); return Buffer.concat([ pictureType, mimeLength, mime, descriptionLength, description, width, height, depth, colors, pictureLength, picture, ]); } buildMetadataBlock(type, block, isLast = false) { const header = Buffer.alloc(4); if (isLast) { type += 128; } header.writeUIntBE(type, 0, 1); header.writeUIntBE(block.length, 1, 3); return Buffer.concat([header, block]); } buildMetadata() { const bufferArray = []; bufferArray.push(this.buildMetadataBlock(STREAMINFO, this.streamInfo)); this.blocks.forEach(block => { bufferArray.push(this.buildMetadataBlock(...block)); }); bufferArray.push(this.buildMetadataBlock(VORBIS_COMMENT, formatVorbisComment(this.vendorString, this.tags))); this.pictures.forEach(block => { bufferArray.push(this.buildMetadataBlock(PICTURE, block)); }); bufferArray.push(this.buildMetadataBlock(PADDING, this.padding, true)); return bufferArray; } buildStream() { const metadata = this.buildMetadata(); return [this.buffer.slice(0, 4), ...metadata, this.buffer.slice(this.framesOffset)]; } /** * Save change to file or return changed buffer. */ save() { if (typeof this.flac === 'string') { dlText(this.flac, Buffer.concat(this.buildStream())); } else { return Buffer.concat(this.buildStream()); } } } return Metaflac; function fetchArrayBuffer(url) { return fetchBlob(url).then(blob => readAsArrayBuffer(blob)); } function fetchBlob(url) { return new Promise(function (resolve, reject) { GM_xmlhttpRequest({ method: 'GET', url, responseType: 'blob', onerror: reject, onload: res => resolve(res.response) }); }); } function readAsArrayBuffer(file) { return new Promise(function (resolve, reject) { const reader = new FileReader(); reader.onload = () => { resolve(reader.result); }; reader.onerror = reject; reader.readAsArrayBuffer(file); }); } function readAsText(file, encoding) { return new Promise(function (resolve, reject) { const reader = new FileReader(); reader.onload = () => { resolve(reader.result); }; reader.onerror = reject; reader.readAsText(file, encoding); }); } // Save text to textfile function dlText(name, text, charset='utf-8') { if (!text || !name) {return false;}; // Get blob url const blob = new Blob([text],{type:`text/plain;charset=${charset}`}); const url = URL.createObjectURL(blob); // Create <a> and download const a = document.createElement('a'); a.href = url; a.download = name; a.click(); } function imageSize(urlOrArrayBuffer) { let url = urlOrArrayBuffer, isObjURL = false; if (typeof url !== 'string') { url = URL.createObjectURL(new Blob([urlOrArrayBuffer])); isObjURL = true; } return new Promise((resolve, reject) => { const img = new Image(); img.src = url; img.onload = () => { resolve({ height: img.naturalHeight, width: img.naturalWidth }); isObjURL && URL.revokeObjectURL(url); } img.onerror = err => { reject(err); isObjURL && URL.revokeObjectURL(url); } }); } function _arrayBufferToBlobURL(buffer) { const blob = new Blob([buffer]); const url = URL.createObjectURL(blob); return url; } })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址