Greasy Fork is available in English.

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)

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