JS FLACMetadataEditor

Allows you to edit metadata of FLAC files. CO

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/40545/646530/JS%20FLACMetadataEditor.js

  1. /* jslint esversion: 6, bitwise: true */
  2.  
  3. // ==UserScript==
  4. // @name JS FLACMetadataEditor
  5. // @description Allows you to edit metadata of FLAC files. CO
  6. // @namespace universe.earth.www.ahohnmyc
  7. // @version 0.0.2.1
  8. // @license GPL-3.0-or-later
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. const FLACMetadataEditor = (()=>{
  13. 'use strict';
  14.  
  15. const _version = '0.0.2.1';
  16.  
  17. class VorbisComment extends Array {}
  18.  
  19. class VorbisCommentPacket {
  20. /* Need to easy initialization */
  21. _addComment(field) {
  22. const value = field.split('=')[1];
  23. field = field.split('=')[0].toUpperCase();
  24. if (!this.hasOwnProperty(field))
  25. this[field] = new VorbisComment();
  26. if (!this[field].some(storedValue=> storedValue===value))
  27. this[field].push(value.toString());
  28. return this;
  29. }
  30. toStringArray() {
  31. const array = [];
  32. Object.keys(this).sort().forEach(key=> {
  33. this[key].forEach(value=> {
  34. array.push(key+'='+value);
  35. });
  36. });
  37. return array;
  38. }
  39. }
  40.  
  41. class FLACMetadataBlockData {}
  42.  
  43. class FLACMetadataBlock {
  44. constructor() {
  45. this.blockType = '';
  46. this.blockTypeNubmer = 0;
  47. this.blockSize = 0;
  48. this.data = new FLACMetadataBlockData();
  49. this.offset = 0;
  50. }
  51. get serializedSize() {
  52. switch (this.blockType) {
  53. case 'STREAMINFO': return 34;
  54. case 'PADDING': return this.blockSize;
  55. case 'APPLICATION': return 4+this.data.applicationData.length;
  56. case 'SEEKTABLE': return this.data.points.length*18;
  57. case 'VORBIS_COMMENT':
  58. const totl = this.data.comments.toStringArray().reduce((sum, str)=>sum+4+str.toUTF8().length, 0);
  59. return 4+this.data.vendorString.length+4+ totl;
  60. case 'CUESHEET': return 0;
  61. 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;
  62. }
  63. }
  64. }
  65.  
  66. class FLACMetadataBlocks extends Array {}
  67.  
  68. class FLACMetadata {
  69. constructor() {
  70. this.blocks = new FLACMetadataBlocks();
  71. this.framesOffset = 0;
  72. this.signature = '';
  73. }
  74. }
  75.  
  76. class _FLACMetadataEditor {
  77. get scriptVersion() {return _version;}
  78.  
  79. constructor(buffer) {
  80. if (!buffer || typeof buffer !== 'object' || !('byteLength' in buffer)) {
  81. throw new Error('First argument should be an instance of ArrayBuffer or Buffer');
  82. }
  83.  
  84. this.arrayBuffer = buffer;
  85. this.metadata = new FLACMetadata();
  86.  
  87. String.prototype.toUTF8 = function(str = null) {
  88. return new TextEncoder().encode(str ? str : this);
  89. };
  90.  
  91. this._parseMetadata();
  92.  
  93. return this;
  94. }
  95.  
  96. /* unpack */
  97. _getBytesAsNumber (array, start=0, end=array.length-start) {return Array.from(array.subarray(start, start+end)).reduce ((result, b)=>result=256*result+b, 0);}
  98. _getBytesAsNumberLittleEndian(array, start=0, end=array.length-start) {return Array.from(array.subarray(start, start+end)).reduceRight((result, b)=>result=256*result+b, 0);}
  99. _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('');}
  100. _getBytesAsUTF8String(array, start=0, end=array.length-start) {return new TextDecoder().decode(array.subarray(start, start+end));}
  101. _getBlockType(number){
  102. switch (number) {
  103. case 0: return 'STREAMINFO';
  104. case 1: return 'PADDING';
  105. case 2: return 'APPLICATION';
  106. case 3: return 'SEEKTABLE';
  107. case 4: return 'VORBIS_COMMENT';
  108. case 5: return 'CUESHEET';
  109. case 6: return 'PICTURE';
  110. case 127: return 'invalid, to avoid confusion with a frame sync code';
  111. default: return 'reserved';
  112. }
  113. }
  114. /* pack */
  115. _uint32ToUint8Array(uint32) {
  116. const eightBitMask = 0xff;
  117. return [
  118. (uint32 >>> 24) & eightBitMask,
  119. (uint32 >>> 16) & eightBitMask,
  120. (uint32 >>> 8) & eightBitMask,
  121. uint32 & eightBitMask,
  122. ];
  123. }
  124. _uint24ToUint8Array(uint32) {
  125. const eightBitMask = 0xff;
  126. return [
  127. (uint32 >>> 16) & eightBitMask,
  128. (uint32 >>> 8) & eightBitMask,
  129. uint32 & eightBitMask,
  130. ];
  131. }
  132. _uint16ToUint8Array(uint32) {
  133. const eightBitMask = 0xff;
  134. return [
  135. (uint32 >>> 8) & eightBitMask,
  136. uint32 & eightBitMask,
  137. ];
  138. }
  139. _hexStringToUint8Array(str) {
  140. return str.replace(/(\w\w)/g,'$1,').slice(0,-1).split(',').map(s=> (parseInt(s[0],16)<<4) + parseInt(s[1],16));
  141. }
  142.  
  143. get _vorbisComment() {
  144. const block = this.metadata.blocks.find(block=>block.blockType==='VORBIS_COMMENT');
  145. if (block)
  146. return block.data;
  147. }
  148.  
  149. addComment(field, value = null) {
  150. if (field) {
  151. if (!value) {
  152. const splitted = field.split('=');
  153. if (!splitted[1]) return this;
  154. value = splitted[1];
  155. field = splitted[0];
  156. }
  157. field = field.toUpperCase();
  158. if (!this._vorbisComment.comments.hasOwnProperty(field))
  159. this._vorbisComment.comments[field] = new VorbisComment();
  160. if (!this._vorbisComment.comments[field].find(storedValue=> storedValue===value))
  161. this._vorbisComment.comments[field].push(value.toString());
  162. }
  163. return this;
  164. }
  165. removeComment(field = null, value = null) {
  166. if (!field) {
  167. Object.keys(this._vorbisComment.comments).forEach(key=> delete this._vorbisComment.comments[key]);
  168. } else {
  169. field = field.toUpperCase();
  170. if (!value) {
  171. delete this._vorbisComment.comments[field];
  172. } else {
  173. value = value.toString();
  174. if (this.hasOwnProperty(field))
  175. this._vorbisComment.comments[field] = this._vorbisComment.comments[field].filter(storedValue=> storedValue!==value);
  176. }
  177. }
  178. return this;
  179. }
  180. getComment(field) {
  181. return this._vorbisComment.comments[field.toUpperCase()];
  182. }
  183.  
  184. addPicture(dataInput) {
  185. if (!dataInput.data || !dataInput.data || typeof dataInput.data !== 'object' || !('byteLength' in dataInput.data)) {
  186. throw new Error('Field "data" should be an instance of ArrayBuffer or Buffer');
  187. }
  188. dataInput.data = new Uint8Array(dataInput.data);
  189.  
  190. const dataDefault = {
  191. APICtype: 3,
  192. MIMEType: 'image/jpeg',
  193. colorDepth: 0,
  194. colorNumber: 0,
  195. data: new Uint8Array([]),
  196. description: '',
  197. width: 0,
  198. height: 0,
  199. };
  200.  
  201. const block = new FLACMetadataBlock();
  202. block.blockTypeNubmer = 6;
  203. block.blockType = 'PICTURE';
  204. for (let property in dataDefault) {
  205. if (dataInput[property]) {
  206. block.data[property] = dataInput[property];
  207. } else {
  208. block.data[property] = dataDefault[property];
  209. }
  210. }
  211.  
  212. const bl = this.metadata.blocks;
  213. let index = bl.length;
  214. if (bl[bl.length-1].blockType === 'PADDING') index--;
  215.  
  216. bl.splice(index, 0, block);
  217. this.metadata.blocks = bl;
  218.  
  219. return this;
  220. }
  221.  
  222.  
  223.  
  224. _serializeMetadataBlock(block) {
  225. const bytes = new Uint8Array(block.serializedSize);
  226. const data = block.data;
  227. let offset = 0;
  228.  
  229. switch (block.blockType) {
  230. case 'STREAMINFO':
  231. bytes.set(this._uint16ToUint8Array(data.minBlockSize));
  232. offset += 2;
  233. bytes.set(this._uint16ToUint8Array(data.maxBlockSize), offset);
  234. offset += 2;
  235. bytes.set(this._uint24ToUint8Array(data.minFrameSize), offset);
  236. offset += 3;
  237. bytes.set(this._uint24ToUint8Array(data.maxFrameSize), offset);
  238. offset += 3;
  239. bytes.set(this._uint24ToUint8Array((data.sampleRate<<4) + (data.numberOfChannels-1<<1) + (data.bitsPerSample-1>>4)), offset);
  240. offset += 3;
  241. bytes[offset] = ((data.bitsPerSample-1&0xF)<<4) + (Math.trunc(data.totalSamples/Math.pow(2,32))&0xF);
  242. offset += 1;
  243. bytes.set(this._uint32ToUint8Array(data.totalSamples), offset);
  244. offset += 4;
  245. bytes.set(this._hexStringToUint8Array(data.rawMD5), offset);
  246. break;
  247. case 'PADDING':
  248. break;
  249. case 'APPLICATION':
  250. bytes.set(data.applicationID.toUTF8());
  251. offset += 4;
  252. bytes.set(data.applicationData, offset);
  253. break;
  254. case 'SEEKTABLE':
  255. data.points.forEach(point=> {
  256. bytes.set(this._hexStringToUint8Array(point.sampleNumber), offset);
  257. bytes.set(this._hexStringToUint8Array(point.offset), offset+8);
  258. bytes.set(this._hexStringToUint8Array(point.numberOfSamples), offset+16);
  259. offset += 18;
  260. });
  261. break;
  262. case 'VORBIS_COMMENT':
  263. bytes.set(this._uint32ToUint8Array(data.vendorString.toUTF8().length).reverse(), offset);
  264. offset += 4;
  265. bytes.set(data.vendorString.toUTF8(), offset);
  266. offset += data.vendorString.toUTF8().length;
  267.  
  268. const comments = data.comments.toStringArray();
  269. bytes.set(this._uint32ToUint8Array(comments.length).reverse(), offset);
  270. offset += 4;
  271. comments.forEach(comment=> {
  272. bytes.set(this._uint32ToUint8Array(comment.toUTF8().length).reverse(), offset);
  273. offset += 4;
  274. bytes.set(comment.toUTF8(), offset);
  275. offset += comment.toUTF8().length;
  276. });
  277. break;
  278. case 'CUESHEET':
  279. break;
  280. case 'PICTURE':
  281. bytes.set(this._uint32ToUint8Array(data.APICtype));
  282. offset += 4;
  283. bytes.set(this._uint32ToUint8Array(data.MIMEType.toUTF8().length), offset);
  284. offset += 4;
  285. bytes.set(data.MIMEType.toUTF8(), offset);
  286. offset += data.MIMEType.toUTF8().length;
  287.  
  288. bytes.set(this._uint32ToUint8Array(data.description.toUTF8().length), offset);
  289. offset += 4;
  290. bytes.set(data.description.toUTF8(), offset);
  291. offset += data.description.toUTF8().length;
  292.  
  293. bytes.set(this._uint32ToUint8Array(data.width), offset);
  294. offset += 4;
  295. bytes.set(this._uint32ToUint8Array(data.height), offset);
  296. offset += 4;
  297. bytes.set(this._uint32ToUint8Array(data.colorDepth), offset);
  298. offset += 4;
  299. bytes.set(this._uint32ToUint8Array(data.colorNumber), offset);
  300. offset += 4;
  301. bytes.set(this._uint32ToUint8Array(data.data.length), offset);
  302. offset += 4;
  303. bytes.set(data.data, offset);
  304. break;
  305. }
  306. return bytes;
  307. }
  308.  
  309. serializeMetadata() {
  310. const newMetadataLengthFull = 4+this.metadata.blocks.reduce((sum, block)=>sum+4+block.serializedSize, 0);
  311. const newSize = newMetadataLengthFull + (this.arrayBuffer.byteLength>this.metadata.framesOffset ? this.arrayBuffer.byteLength-this.metadata.framesOffset : 0);
  312.  
  313. const bytes = new Uint8Array(newSize);
  314. bytes.set(this.metadata.signature.toUTF8());
  315.  
  316. let offset = 4;
  317. let lastBlock = false;
  318. this.metadata.blocks.forEach((block, n, blocks)=>{
  319. if (blocks.length-1 === n) lastBlock = true;
  320. bytes[offset] = block.blockTypeNubmer | (lastBlock<<7);
  321. offset += 1;
  322. bytes.set(this._uint24ToUint8Array(block.serializedSize), offset);
  323. offset += 3;
  324.  
  325. bytes.set(this._serializeMetadataBlock(block), offset);
  326. offset += block.serializedSize;
  327. });
  328.  
  329. // console.info('old meta size: %d, new: %d, delta: %d', this.metadata.framesOffset, newMetadataLengthFull, Math.abs(this.metadata.framesOffset-newMetadataLengthFull) );
  330. // console.info('old size: %d, new: %d, delta: %d', this.arrayBuffer.byteLength, newSize, Math.abs(this.arrayBuffer.byteLength-newSize) );
  331. // console.info('frames size: %d, to copy: %d', this.arrayBuffer.byteLength-this.metadata.framesOffset, new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset).length);
  332. // console.info('offset: %d', offset );
  333.  
  334. bytes.set(new Uint8Array(this.arrayBuffer).subarray(this.metadata.framesOffset), offset);
  335.  
  336. this.arrayBuffer = bytes.buffer;
  337. return this;
  338. }
  339.  
  340.  
  341. _parseMetadataBlock(array, arrayOffset, type, size) {
  342. const blockData = array.subarray(arrayOffset, arrayOffset+size);
  343. let offset = 0;
  344. const data = new FLACMetadataBlockData();
  345. switch (type) {
  346. case 'STREAMINFO':
  347. data.minBlockSize = this._getBytesAsNumber(blockData, offset, 2);
  348. offset += 2;
  349. data.maxBlockSize = this._getBytesAsNumber(blockData, offset, 2);
  350. offset += 2;
  351. data.minFrameSize = this._getBytesAsNumber(blockData, offset, 3);
  352. offset += 3;
  353. data.maxFrameSize = this._getBytesAsNumber(blockData, offset, 3);
  354. offset += 3;
  355. data.sampleRate = this._getBytesAsNumber(blockData, offset, 3)>>4;
  356. offset += 2;
  357. data.numberOfChannels = 1+ ((blockData[offset]>>1) &7);
  358. data.bitsPerSample = 1+ ((1&blockData[offset]) <<4) + (blockData[offset+1]>>4);
  359. offset += 1;
  360. data.totalSamples = (blockData[offset]&0xF)*Math.pow(2,32) + this._getBytesAsNumber(blockData, offset+1, 4);
  361. offset += 5;
  362. data.rawMD5 = this._getBytesAsHexString(blockData, offset, 16).toUpperCase();
  363. break;
  364. case 'PADDING':
  365. break;
  366. case 'APPLICATION':
  367. data.applicationID = this._getBytesAsUTF8String(blockData, offset, 4);
  368. offset += 4;
  369. data.applicationData = blockData.subarray(offset);
  370. break;
  371. case 'SEEKTABLE':
  372. data.pointCount = size/18;
  373. data.points = [];
  374. for (let i=0; i<data.pointCount; i++) {
  375. data.points.push({
  376. sampleNumber: this._getBytesAsHexString(blockData, offset, 8),
  377. offset: this._getBytesAsHexString(blockData, offset+8, 8),
  378. numberOfSamples: this._getBytesAsHexString(blockData, offset+16, 2),
  379. });
  380. offset += 18;
  381. }
  382. break;
  383. case 'VORBIS_COMMENT':
  384. const vendorLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4);
  385. offset += 4;
  386. data.vendorString = this._getBytesAsUTF8String(blockData, offset, vendorLength);
  387. offset += vendorLength;
  388.  
  389. const userCommentListLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4);
  390. offset += 4;
  391. data.comments = new VorbisCommentPacket();
  392.  
  393. let commentLength = 0;
  394. for (let i=0; i<userCommentListLength; i++) {
  395. commentLength = this._getBytesAsNumberLittleEndian(blockData, offset, 4);
  396. offset += 4;
  397. data.comments._addComment(this._getBytesAsUTF8String(blockData, offset, commentLength));
  398. offset += commentLength;
  399. }
  400. break;
  401. case 'CUESHEET':
  402. break;
  403. case 'PICTURE':
  404. data.APICtype = this._getBytesAsNumber(blockData, offset, 4);
  405. offset += 4;
  406. const MIMELength = this._getBytesAsNumber(blockData, offset, 4);
  407. offset += 4;
  408. data.MIMEType = this._getBytesAsUTF8String(blockData, offset, MIMELength);
  409. offset += MIMELength;
  410. const descriptionLength = this._getBytesAsNumber(blockData, offset, 4);
  411. offset += 4;
  412. data.description = this._getBytesAsUTF8String(blockData, offset, descriptionLength);
  413. offset += descriptionLength;
  414. data.width = this._getBytesAsNumber(blockData, offset, 4);
  415. offset += 4;
  416. data.height = this._getBytesAsNumber(blockData, offset, 4);
  417. offset += 4;
  418. data.colorDepth = this._getBytesAsNumber(blockData, offset, 4);
  419. offset += 4;
  420. data.colorNumber = this._getBytesAsNumber(blockData, offset, 4);
  421. offset += 4;
  422. const binarySize = this._getBytesAsNumber(blockData, offset, 4);
  423. offset += 4;
  424. data.data = blockData.subarray(offset, offset+binarySize);
  425. break;
  426. }
  427. return data;
  428. }
  429.  
  430. _parseMetadata() {
  431. const bytes = new Uint8Array(this.arrayBuffer);
  432.  
  433. this.metadata.signature = this._getBytesAsUTF8String(bytes,0,4);
  434.  
  435. let offset = 4;
  436. let lastBlock = false;
  437. let block;
  438.  
  439. let iteration = 0;
  440. while (!lastBlock && offset < bytes.length) {
  441. if (iteration++ > 42) throw new RangeError('Too much METADATA_BLOCKS. Looks like file corrupted');
  442.  
  443. block = new FLACMetadataBlock();
  444.  
  445. block.offset = offset;
  446. lastBlock = !!(bytes[offset] >> 7);
  447. block.blockTypeNubmer = bytes[offset] & 127;
  448. block.blockType = this._getBlockType(block.blockTypeNubmer);
  449. offset += 1;
  450. block.blockSize = this._getBytesAsNumber(bytes, offset, 3);
  451. offset += 3;
  452. block.data = this._parseMetadataBlock(bytes, offset, block.blockType, block.blockSize);
  453. offset += block.blockSize;
  454.  
  455. // if (block.blockType !== 'PADDING')
  456. this.metadata.blocks.push(block);
  457. }
  458. this.metadata.framesOffset = offset;
  459. return this;
  460. }
  461. }
  462.  
  463. return _FLACMetadataEditor;
  464. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址