From 80ce4ee7e1a6901c94164bc4a7d3cfbf4a339cbc Mon Sep 17 00:00:00 2001 From: Borewit Date: Wed, 16 Jul 2025 16:13:09 +0200 Subject: [PATCH] Migrate `music-metadata-browser` to `music-metadata` --- docs/README.md | 4 +- package.json | 4 +- src/index.js | 111 ++++++++++++++++++++++++++++------------------ webpack.config.js | 4 -- webpack.dev.js | 4 -- 5 files changed, 71 insertions(+), 56 deletions(-) diff --git a/docs/README.md b/docs/README.md index f9073f3..0ceb2d4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -65,12 +65,10 @@ And all the developers and artists, for the following resources: ### JavaScript libraries -* [buffer](https://www.npmjs.com/package/buffer) - Node.js Buffer API, for the browser * [http-server](https://www.npmjs.com/package/http-server) - a simple zero-configuration command-line http server * [idb-keyval](https://www.npmjs.com/package/idb-keyval) - super-simple promise-based keyval store implemented with IndexedDB -* [music-metadata-browser](https://www.npmjs.com/package/music-metadata-browser) - stream and file based music metadata parser for the browser +* [music-metadata](https://www.npmjs.com/package/music-metadata) - Metadata parser for audio and video media files. Supports file and stream inputs in Node.js and browser environments, extracting format, tag, and duration information. * [notie](https://www.npmjs.com/package/notie) - clean and simple notification, input, and selection suite for javascript, with no dependencies -* [process](https://www.npmjs.com/package/process) - process information for node.js and browsers * [scrollIntoViewIfNeeded 4 everyone](https://gist.github.com/hsablonniere/2581101) - polyfill for non-standard scrollIntoViewIfNeeded() method * [sortablejs](https://www.npmjs.com/package/sortablejs) - JavaScript library for reorderable drag-and-drop lists * [webpack](https://www.npmjs.com/package/webpack) - JavaScript module bundler for the browser diff --git a/package.json b/package.json index 0a7c457..6c2f08b 100644 --- a/package.json +++ b/package.json @@ -12,16 +12,14 @@ }, "devDependencies": { "audiomotion-analyzer": "^4.5.1", - "buffer": "^6.0.3", "css-loader": "^7.1.2", "css-minimizer-webpack-plugin": "^7.0.0", "http-server": "^14.1.1", "idb-keyval": "^6.2.1", "js-yaml": "^4.1.0", "mini-css-extract-plugin": "^2.9.0", - "music-metadata-browser": "^2.5.10", + "music-metadata": "^11.7.1", "notie": "^4.3.1", - "process": "^0.11.10", "sortablejs": "^1.15.2", "style-loader": "^4.0.0", "webpack": "^5.91.0", diff --git a/src/index.js b/src/index.js index 86a90f9..d69008b 100644 --- a/src/index.js +++ b/src/index.js @@ -32,7 +32,7 @@ import AudioMotionAnalyzer from 'audiomotion-analyzer'; import packageJson from '../package.json'; import * as fileExplorer from './file-explorer.js'; -import * as mm from 'music-metadata-browser'; +import {parseBlob, parseWebStream} from 'music-metadata'; import './scrollIntoViewIfNeeded-polyfill.js'; import { get, set, del } from 'idb-keyval'; import * as yaml from 'js-yaml'; @@ -2017,7 +2017,7 @@ function keyboardControls( event ) { } /** - * Sets (or removes) the `src` attribute of a audio element and + * Sets (or removes) the `src` attribute of an audio element and * releases any data blob (File System API) previously in use by it * * @param {object} audio element @@ -2121,7 +2121,8 @@ function loadGradientIntoCurrentGradient(gradientKey) { /** * Load a music file from the user's computer */ -function loadLocalFile( obj ) { +async function loadLocalFile( obj ) { + const fileBlob = obj.files[0]; if ( fileBlob ) { @@ -2130,11 +2131,17 @@ function loadLocalFile( obj ) { audioEl.dataset.file = fileBlob.name; audioEl.dataset.title = parsePath( fileBlob.name ).baseName; - // load and play - loadFileBlob( fileBlob, audioEl, true ) - .then( url => mm.fetchFromUrl( url ) ) - .then( metadata => addMetadata( metadata, audioEl ) ) - .catch( e => {} ); + try { + // Start both tasks, but only await parseBlob immediately + const loadTask = loadFileBlob(fileBlob, audioEl, true); + const metadata = await parseBlob( fileBlob ); + await addMetadata(metadata, audioEl); + + // Wait for loadTask to complete + await loadTask; + } catch( error ) { + consoleLog("Failed to load local file", error); + } } } @@ -3295,60 +3302,80 @@ async function retrieveBackgrounds() { } /** - * Retrieve metadata for files in the play queue + * Retrieve metadata for the first MAX_METADATA_REQUESTS files in the play queue, + * which have no metadata assigned yet */ async function retrieveMetadata() { // leave when we already have enough concurrent requests pending + // ToDo: + // Very arguable way to restrict parallel processing + // Without calling this to often, leaves items unprocessed if ( waitingMetadata >= MAX_METADATA_REQUESTS ) return; - // find the first play queue item for which we haven't retrieved the metadata yet - const queueItem = Array.from( playlist.children ).find( el => el.dataset.retrieve ); + ++waitingMetadata; - if ( queueItem ) { + // Process in parallel + for ( const queueItem of playlist.children ) { - let uri = queueItem.dataset.file, - revoke = false; - - waitingMetadata++; + if (!queueItem.dataset.retrieve) continue; delete queueItem.dataset.retrieve; - queryMetadata: { + let metadata; + let file; + + try { if ( queueItem.handle ) { - try { - if ( await queueItem.handle.requestPermission() != 'granted' ) - break queryMetadata; - uri = URL.createObjectURL( await queueItem.handle.getFile() ); - revoke = true; - } - catch( e ) { - break queryMetadata; - } - } + // Fetch metadata from File object + if (await queueItem.handle.requestPermission() !== 'granted') + return; - try { - const metadata = await mm.fetchFromUrl( uri, { skipPostHeaders: true } ); - if ( metadata ) { - addMetadata( metadata, queueItem ); // add metadata to play queue item - syncMetadataToAudioElements( queueItem ); - if ( ! ( metadata.common.picture && metadata.common.picture.length ) ) { - getFolderCover( queueItem ).then( cover => { - queueItem.dataset.cover = cover; - syncMetadataToAudioElements( queueItem ); - }); + file = await queueItem.handle.getFile(); + metadata = await parseBlob(file); + } + else + { + // Fetch metadata from URI + const response = await fetch(queueItem.dataset.file); + if (response.ok) { + if (response.body?.getReader) { + const contentType = response.headers.get("Content-Type"); + const contentSize = response.headers.get("Content-Length"); + try { + metadata = await parseWebStream(response.body, { + mimeType: contentType, + size: contentSize ? Number.parseInt(contentSize, 10) : undefined + }, {skipPostHeaders: true}); + } finally { + await response.body.cancel(); + } + } else { + // Fallback to Blob, in case the HTTP Result cannot be streamed + metadata = await parseBlob(await response.blob()); } + } else { + consoleLog(`Failed to fetch metadata http-response=${response.status} for url=${queueItem.dataset.file}`, true); } } - catch( e ) {} + } + catch( e ) { + consoleLog(`Error converting queued file="${queueItem.dataset.file ?? '?'}" to URI`, e); + return; + } - if ( revoke ) - URL.revokeObjectURL( uri ); + console.log(`Fetched metadata successful for url=${queueItem.dataset.file}`); + addMetadata( metadata, queueItem ); // add metadata to play queue item + + // If no embedded picture, try folder cover + if ( ! ( metadata.common.picture && metadata.common.picture.length > 0) ) { + queueItem.dataset.cover = await getFolderCover( queueItem ); } - waitingMetadata--; - retrieveMetadata(); // call again to continue processing the queue + syncMetadataToAudioElements( queueItem ); } + + --waitingMetadata; } /** diff --git a/webpack.config.js b/webpack.config.js index f598811..197e572 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -36,10 +36,6 @@ module.exports = { new MiniCssExtractPlugin({ filename: 'styles.css', }), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - process: 'process/browser.js', - }), ], output: { filename: pathData => { diff --git a/webpack.dev.js b/webpack.dev.js index 83d74a8..5f6adbd 100644 --- a/webpack.dev.js +++ b/webpack.dev.js @@ -35,10 +35,6 @@ module.exports = { }, plugins: [ new webpack.HotModuleReplacementPlugin(), - new webpack.ProvidePlugin({ - Buffer: ['buffer', 'Buffer'], - process: 'process/browser.js', - }), ], output: { filename: 'audioMotion.js',