metaflac.js

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)

当前为 2023-12-18 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/482520/1297739/metaflacjs.js

  1. /* eslint-disable no-multi-spaces */
  2. /* eslint-disable no-return-assign */
  3.  
  4. // ==UserScript==
  5. // @name metaflac.js
  6. // @namespace https://github.com/ishowshao/metaflac-js/
  7. // @version 0.1
  8. // @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)
  9. // @author ishowshao, PY-DNG
  10. // @license https://github.com/ishowshao/metaflac-js/
  11. // ==/UserScript==
  12.  
  13. /* global Buffer */
  14. // !IMPORTANT! THIS USERSCRIPT LIBRARY REQUIRES https://gf.qytechs.cn/scripts/482519-buffer TO WORK!
  15.  
  16. let Metaflac = (async function __MAIN__() {
  17. 'use strict';
  18.  
  19. const FileType = await import('https://cdn.jsdelivr.net/npm/file-type@18.7.0/+esm');
  20. const formatVorbisComment = (function() {
  21. return (vendorString, commentList) => {
  22. const bufferArray = [];
  23. const vendorStringBuffer = Buffer.from(vendorString, 'utf8');
  24. const vendorLengthBuffer = Buffer.alloc(4);
  25. vendorLengthBuffer.writeUInt32LE(vendorStringBuffer.length);
  26.  
  27. const userCommentListLengthBuffer = Buffer.alloc(4);
  28. userCommentListLengthBuffer.writeUInt32LE(commentList.length);
  29.  
  30. bufferArray.push(vendorLengthBuffer, vendorStringBuffer, userCommentListLengthBuffer);
  31.  
  32. for (let i = 0; i < commentList.length; i++) {
  33. const comment = commentList[i];
  34. const commentBuffer = Buffer.from(comment, 'utf8');
  35. const lengthBuffer = Buffer.alloc(4);
  36. lengthBuffer.writeUInt32LE(commentBuffer.length);
  37. bufferArray.push(lengthBuffer, commentBuffer);
  38. }
  39.  
  40. return Buffer.concat(bufferArray);
  41. }
  42. })();
  43.  
  44. const BLOCK_TYPE = {
  45. 0: 'STREAMINFO',
  46. 1: 'PADDING',
  47. 2: 'APPLICATION',
  48. 3: 'SEEKTABLE',
  49. 4: 'VORBIS_COMMENT', // There may be only one VORBIS_COMMENT block in a stream.
  50. 5: 'CUESHEET',
  51. 6: 'PICTURE',
  52. };
  53.  
  54. const STREAMINFO = 0;
  55. const PADDING = 1;
  56. const APPLICATION = 2;
  57. const SEEKTABLE = 3;
  58. const VORBIS_COMMENT = 4;
  59. const CUESHEET = 5;
  60. const PICTURE = 6;
  61.  
  62. class Metaflac {
  63. constructor(flac) {
  64. if (typeof flac !== 'string' && typeof flac !== 'string' && !Buffer.isBuffer(flac)) {
  65. throw new Error('Metaflac(flac) flac must be string or buffer.');
  66. }
  67. this.flac = flac;
  68. this.buffer = null;
  69. this.marker = '';
  70. this.streamInfo = null;
  71. this.blocks = [];
  72. this.padding = null;
  73. this.vorbisComment = null;
  74. this.vendorString = '';
  75. this.tags = [];
  76. this.pictures = [];
  77. this.picturesSpecs = [];
  78. this.picturesDatas = [];
  79. this.framesOffset = 0;
  80. this.init();
  81. }
  82.  
  83. async init() {
  84. typeof this.flac === 'string' ? this.buffer = await fetchArrayBuffer(this.flac) : this.buffer = this.flac;
  85.  
  86. let offset = 0;
  87. const marker = this.buffer.slice(0, offset += 4).toString('ascii');
  88. if (marker !== 'fLaC') {
  89. throw new Error('The file does not appear to be a FLAC file.');
  90. }
  91.  
  92. let blockType = 0;
  93. let isLastBlock = false;
  94. while (!isLastBlock) {
  95. blockType = this.buffer.readUInt8(offset++);
  96. isLastBlock = blockType > 128;
  97. blockType = blockType % 128;
  98. // console.log('Block Type: %d %s', blockType, BLOCK_TYPE[blockType]);
  99.  
  100. const blockLength = this.buffer.readUIntBE(offset, 3);
  101. offset += 3;
  102.  
  103. if (blockType === STREAMINFO) {
  104. this.streamInfo = this.buffer.slice(offset, offset + blockLength);
  105. }
  106.  
  107. if (blockType === PADDING) {
  108. this.padding = this.buffer.slice(offset, offset + blockLength);
  109. }
  110.  
  111. if (blockType === VORBIS_COMMENT) {
  112. this.vorbisComment = this.buffer.slice(offset, offset + blockLength);
  113. this.parseVorbisComment();
  114. }
  115.  
  116. if (blockType === PICTURE) {
  117. this.pictures.push(this.buffer.slice(offset, offset + blockLength));
  118. this.parsePictureBlock();
  119. }
  120.  
  121. if ([APPLICATION, SEEKTABLE, CUESHEET].includes(blockType)) {
  122. this.blocks.push([blockType, this.buffer.slice(offset, offset + blockLength)]);
  123. }
  124. // console.log('Block Length: %d', blockLength);
  125. offset += blockLength;
  126. }
  127. this.framesOffset = offset;
  128. }
  129.  
  130. parseVorbisComment() {
  131. const vendorLength = this.vorbisComment.readUInt32LE(0);
  132. // console.log('Vendor length: %d', vendorLength);
  133. this.vendorString = this.vorbisComment.slice(4, vendorLength + 4).toString('utf8');
  134. // console.log('Vendor string: %s', this.vendorString);
  135. const userCommentListLength = this.vorbisComment.readUInt32LE(4 + vendorLength);
  136. // console.log('user_comment_list_length: %d', userCommentListLength);
  137. const userCommentListBuffer = this.vorbisComment.slice(4 + vendorLength + 4);
  138. for (let offset = 0; offset < userCommentListBuffer.length; ) {
  139. const length = userCommentListBuffer.readUInt32LE(offset);
  140. offset += 4;
  141. const comment = userCommentListBuffer.slice(offset, offset += length).toString('utf8');
  142. // console.log('Comment length: %d, this.buffer: %s', length, comment);
  143. this.tags.push(comment);
  144. }
  145. }
  146.  
  147. parsePictureBlock() {
  148. this.pictures.forEach(picture => {
  149. let offset = 0;
  150. const type = picture.readUInt32BE(offset);
  151. offset += 4;
  152. const mimeTypeLength = picture.readUInt32BE(offset);
  153. offset += 4;
  154. const mime = picture.slice(offset, offset + mimeTypeLength).toString('ascii');
  155. offset += mimeTypeLength;
  156. const descriptionLength = picture.readUInt32BE(offset);
  157. offset += 4;
  158. const description = picture.slice(offset, offset += descriptionLength).toString('utf8');
  159. const width = picture.readUInt32BE(offset);
  160. offset += 4;
  161. const height = picture.readUInt32BE(offset);
  162. offset += 4;
  163. const depth = picture.readUInt32BE(offset);
  164. offset += 4;
  165. const colors = picture.readUInt32BE(offset);
  166. offset += 4;
  167. const pictureDataLength = picture.readUInt32BE(offset);
  168. offset += 4;
  169. this.picturesDatas.push(picture.slice(offset, offset + pictureDataLength));
  170. this.picturesSpecs.push(this.buildSpecification({
  171. type,
  172. mime,
  173. description,
  174. width,
  175. height,
  176. depth,
  177. colors
  178. }));
  179. });
  180. }
  181.  
  182. getPicturesSpecs() {
  183. return this.picturesSpecs;
  184. }
  185.  
  186. /**
  187. * Get the MD5 signature from the STREAMINFO block.
  188. */
  189. getMd5sum() {
  190. return this.streamInfo.slice(18, 34).toString('hex');
  191. }
  192.  
  193. /**
  194. * Get the minimum block size from the STREAMINFO block.
  195. */
  196. getMinBlocksize() {
  197. return this.streamInfo.readUInt16BE(0);
  198. }
  199.  
  200. /**
  201. * Get the maximum block size from the STREAMINFO block.
  202. */
  203. getMaxBlocksize() {
  204. return this.streamInfo.readUInt16BE(2);
  205. }
  206.  
  207. /**
  208. * Get the minimum frame size from the STREAMINFO block.
  209. */
  210. getMinFramesize() {
  211. return this.streamInfo.readUIntBE(4, 3);
  212. }
  213.  
  214. /**
  215. * Get the maximum frame size from the STREAMINFO block.
  216. */
  217. getMaxFramesize() {
  218. return this.streamInfo.readUIntBE(7, 3);
  219. }
  220.  
  221. /**
  222. * Get the sample rate from the STREAMINFO block.
  223. */
  224. getSampleRate() {
  225. // 20 bits number
  226. return this.streamInfo.readUIntBE(10, 3) >> 4;
  227. }
  228.  
  229. /**
  230. * Get the number of channels from the STREAMINFO block.
  231. */
  232. getChannels() {
  233. // 3 bits
  234. return this.streamInfo.readUIntBE(10, 3) & 0x00000f >> 1;
  235. }
  236.  
  237. /**
  238. * Get the # of bits per sample from the STREAMINFO block.
  239. */
  240. getBps() {
  241. return this.streamInfo.readUIntBE(12, 2) & 0x01f0 >> 4;
  242. }
  243.  
  244. /**
  245. * Get the total # of samples from the STREAMINFO block.
  246. */
  247. getTotalSamples() {
  248. return this.streamInfo.readUIntBE(13, 5) & 0x0fffffffff;
  249. }
  250.  
  251. /**
  252. * Show the vendor string from the VORBIS_COMMENT block.
  253. */
  254. getVendorTag() {
  255. return this.vendorString;
  256. }
  257.  
  258. /**
  259. * Get all tags where the the field name matches NAME.
  260. *
  261. * @param {string} name
  262. */
  263. getTag(name) {
  264. return this.tags.filter(item => {
  265. const itemName = item.split('=')[0];
  266. return itemName === name;
  267. }).join('\n');
  268. }
  269.  
  270. /**
  271. * Remove all tags whose field name is NAME.
  272. *
  273. * @param {string} name
  274. */
  275. removeTag(name) {
  276. this.tags = this.tags.filter(item => {
  277. const itemName = item.split('=')[0];
  278. return itemName !== name;
  279. });
  280. }
  281.  
  282. /**
  283. * Remove first tag whose field name is NAME.
  284. *
  285. * @param {string} name
  286. */
  287. removeFirstTag(name) {
  288. const found = this.tags.findIndex(item => {
  289. return item.split('=')[0] === name;
  290. });
  291. if (found !== -1) {
  292. this.tags.splice(found, 1);
  293. }
  294. }
  295.  
  296. /**
  297. * Remove all tags, leaving only the vendor string.
  298. */
  299. removeAllTags() {
  300. this.tags = [];
  301. }
  302.  
  303. /**
  304. * Add a tag.
  305. * 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.
  306. *
  307. * @param {string} field
  308. */
  309. setTag(field) {
  310. if (field.indexOf('=') === -1) {
  311. throw new Error(`malformed vorbis comment field "${field}", field contains no '=' character`);
  312. }
  313. this.tags.push(field);
  314. }
  315.  
  316. /**
  317. * Like setTag, except the VALUE is a filename whose contents will be read verbatim to set the tag value.
  318. *
  319. * @param {string} field
  320. */
  321. async setTagFromFile(field) {
  322. const position = field.indexOf('=');
  323. if (position === -1) {
  324. throw new Error(`malformed vorbis comment field "${field}", field contains no '=' character`);
  325. }
  326. const name = field.substring(0, position);
  327. const filename = field.substr(position + 1);
  328. let value;
  329. try {
  330. value = await readAsText(filename, 'utf8');
  331. } catch (e) {
  332. throw new Error(`can't open file '${filename}' for '${name}' tag value`);
  333. }
  334. this.tags.push(`${name}=${value}`);
  335. }
  336.  
  337. /**
  338. * Import tags from a file.
  339. * Each line should be of the form NAME=VALUE.
  340. *
  341. * @param {string} filename
  342. */
  343. async importTagsFrom(filename) {
  344. const tags = await readAsText(filename, 'utf8').split('\n');
  345. tags.forEach(line => {
  346. if (line.indexOf('=') === -1) {
  347. throw new Error(`malformed vorbis comment "${line}", contains no '=' character`);
  348. }
  349. });
  350. this.tags = this.tags.concat(tags);
  351. }
  352.  
  353. /**
  354. * Export tags to a file.
  355. * Each line will be of the form NAME=VALUE.
  356. *
  357. * @param {string} filename
  358. */
  359. exportTagsTo(filename) {
  360. dlText(filename, this.tags.join('\n'), 'utf8');
  361. }
  362.  
  363. /**
  364. * Import a picture and store it in a PICTURE metadata block.
  365. *
  366. * @param {string} filename
  367. */
  368. async importPictureFrom(filename) {
  369. const picture = await fetchArrayBuffer(filename);
  370. const {mime} = await FileType.fileTypeFromBuffer(picture);
  371. if (mime !== 'image/jpeg' && mime !== 'image/png') {
  372. throw new Error(`only support image/jpeg and image/png picture temporarily, current import ${mime}`);
  373. }
  374. const dimensions = await imageSize(filename);
  375. const spec = this.buildSpecification({
  376. mime: mime,
  377. width: dimensions.width,
  378. height: dimensions.height,
  379. });
  380. this.pictures.push(this.buildPictureBlock(picture, spec));
  381. this.picturesSpecs.push(spec);
  382. }
  383.  
  384. /**
  385. * Import a picture and store it in a PICTURE metadata block.
  386. *
  387. * @param {Buffer} picture
  388. */
  389. async importPictureFromBuffer(picture) {
  390. const {mime} = await FileType.fileTypeFromBuffer(picture);
  391. if (mime !== 'image/jpeg' && mime !== 'image/png') {
  392. throw new Error(`only support image/jpeg and image/png picture temporarily, current import ${mime}`);
  393. }
  394. const dimensions = await imageSize(picture);
  395. const spec = this.buildSpecification({
  396. mime: mime,
  397. width: dimensions.width,
  398. height: dimensions.height,
  399. });
  400. this.pictures.push(this.buildPictureBlock(picture, spec));
  401. this.picturesSpecs.push(spec);
  402. }
  403.  
  404. /**
  405. * Export PICTURE block to a file.
  406. *
  407. * @param {string} filename
  408. */
  409. exportPictureTo(filename) {
  410. if (this.picturesDatas.length > 0) {
  411. dlText(filename, this.picturesDatas[0]);
  412. }
  413. }
  414.  
  415. /**
  416. * Return all tags.
  417. */
  418. getAllTags() {
  419. return this.tags;
  420. }
  421.  
  422. buildSpecification(spec = {}) {
  423. const defaults = {
  424. type: 3,
  425. mime: 'image/jpeg',
  426. description: '',
  427. width: 0,
  428. height: 0,
  429. depth: 24,
  430. colors: 0,
  431. };
  432. return Object.assign(defaults, spec);
  433. }
  434.  
  435. /**
  436. * Build a picture block.
  437. *
  438. * @param {Buffer} picture
  439. * @param {Object} specification
  440. * @returns {Buffer}
  441. */
  442. buildPictureBlock(picture, specification = {}) {
  443. const pictureType = Buffer.alloc(4);
  444. const mimeLength = Buffer.alloc(4);
  445. const mime = Buffer.from(specification.mime, 'ascii');
  446. const descriptionLength = Buffer.alloc(4);
  447. const description = Buffer.from(specification.description, 'utf8');
  448. const width = Buffer.alloc(4);
  449. const height = Buffer.alloc(4);
  450. const depth = Buffer.alloc(4);
  451. const colors = Buffer.alloc(4);
  452. const pictureLength = Buffer.alloc(4);
  453.  
  454. pictureType.writeUInt32BE(specification.type);
  455. mimeLength.writeUInt32BE(specification.mime.length);
  456. descriptionLength.writeUInt32BE(specification.description.length);
  457. width.writeUInt32BE(specification.width);
  458. height.writeUInt32BE(specification.height);
  459. depth.writeUInt32BE(specification.depth);
  460. colors.writeUInt32BE(specification.colors);
  461. pictureLength.writeUInt32BE(picture.length);
  462.  
  463. return Buffer.concat([
  464. pictureType,
  465. mimeLength,
  466. mime,
  467. descriptionLength,
  468. description,
  469. width,
  470. height,
  471. depth,
  472. colors,
  473. pictureLength,
  474. picture,
  475. ]);
  476. }
  477.  
  478. buildMetadataBlock(type, block, isLast = false) {
  479. const header = Buffer.alloc(4);
  480. if (isLast) {
  481. type += 128;
  482. }
  483. header.writeUIntBE(type, 0, 1);
  484. header.writeUIntBE(block.length, 1, 3);
  485. return Buffer.concat([header, block]);
  486. }
  487.  
  488. buildMetadata() {
  489. const bufferArray = [];
  490. bufferArray.push(this.buildMetadataBlock(STREAMINFO, this.streamInfo));
  491. this.blocks.forEach(block => {
  492. bufferArray.push(this.buildMetadataBlock(...block));
  493. });
  494. bufferArray.push(this.buildMetadataBlock(VORBIS_COMMENT, formatVorbisComment(this.vendorString, this.tags)));
  495. this.pictures.forEach(block => {
  496. bufferArray.push(this.buildMetadataBlock(PICTURE, block));
  497. });
  498. bufferArray.push(this.buildMetadataBlock(PADDING, this.padding, true));
  499. return bufferArray;
  500. }
  501.  
  502. buildStream() {
  503. const metadata = this.buildMetadata();
  504. return [this.buffer.slice(0, 4), ...metadata, this.buffer.slice(this.framesOffset)];
  505. }
  506.  
  507. /**
  508. * Save change to file or return changed buffer.
  509. */
  510. save() {
  511. if (typeof this.flac === 'string') {
  512. dlText(this.flac, Buffer.concat(this.buildStream()));
  513. } else {
  514. return Buffer.concat(this.buildStream());
  515. }
  516. }
  517. }
  518.  
  519. return Metaflac;
  520.  
  521. function fetchArrayBuffer(url) {
  522. return fetchBlob(url).then(blob => readAsArrayBuffer(blob));
  523. }
  524.  
  525. function fetchBlob(url) {
  526. return new Promise(function (resolve, reject) {
  527. GM_xmlhttpRequest({
  528. method: 'GET', url,
  529. responseType: 'blob',
  530. onerror: reject,
  531. onload: res => resolve(res.response)
  532. });
  533. });
  534. }
  535.  
  536. function readAsArrayBuffer(file) {
  537. return new Promise(function (resolve, reject) {
  538. const reader = new FileReader();
  539. reader.onload = () => {
  540. resolve(reader.result);
  541. };
  542.  
  543. reader.onerror = reject;
  544. reader.readAsArrayBuffer(file);
  545. });
  546. }
  547.  
  548. function readAsText(file, encoding) {
  549. return new Promise(function (resolve, reject) {
  550. const reader = new FileReader();
  551. reader.onload = () => {
  552. resolve(reader.result);
  553. };
  554.  
  555. reader.onerror = reject;
  556. reader.readAsText(file, encoding);
  557. });
  558. }
  559.  
  560. // Save text to textfile
  561. function dlText(name, text, charset='utf-8') {
  562. if (!text || !name) {return false;};
  563.  
  564. // Get blob url
  565. const blob = new Blob([text],{type:`text/plain;charset=${charset}`});
  566. const url = URL.createObjectURL(blob);
  567.  
  568. // Create <a> and download
  569. const a = document.createElement('a');
  570. a.href = url;
  571. a.download = name;
  572. a.click();
  573. }
  574.  
  575. function imageSize(urlOrArrayBuffer) {
  576. const url = typeof urlOrArrayBuffer === 'string' ? urlOrArrayBuffer : _arrayBufferToBase64(urlOrArrayBuffer);
  577. return new Promise((resolve, reject) => {
  578. const img = new Image();
  579. img.src = url;
  580. img.onload = () => resolve({
  581. height: img.naturalHeight,
  582. width: img.naturalWidth
  583. });
  584. img.onerror = err => reject(err);
  585. });
  586. }
  587.  
  588. function _arrayBufferToBase64( buffer ) {
  589. var binary = '';
  590. var bytes = new Uint8Array( buffer );
  591. var len = bytes.byteLength;
  592. for (var i = 0; i < len; i++) {
  593. binary += String.fromCharCode( bytes[ i ] );
  594. }
  595. return window.btoa( binary );
  596. }
  597.  
  598. })();

QingJ © 2025

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