feat(downloads): use taglib-wasm to set tags

taglib supports multiple media formats beyond what was previously supported, this would enable transcoding to other formats without needing to write additional metadata libraries.
This commit is contained in:
Daniel 2026-03-03 19:42:54 +00:00 committed by GitHub
parent c7a4ba194d
commit 50a5b79d70
9 changed files with 391 additions and 1301 deletions

453
bun.lock

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,7 @@ import { addMetadataToAudio } from './metadata.js';
import { DashDownloader } from './dash-downloader.js'; import { DashDownloader } from './dash-downloader.js';
import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js'; import { encodeToMp3, MP3EncodingError } from './mp3-encoder.js';
import { ffmpeg } from './ffmpeg.js'; import { ffmpeg } from './ffmpeg.js';
import { initTagLib } from './taglib.js';
export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE'; export const DASH_MANIFEST_UNAVAILABLE_CODE = 'DASH_MANIFEST_UNAVAILABLE';
const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25'; const TIDAL_V2_TOKEN = 'txNoH4kkV41MfH25';
@ -1109,6 +1110,8 @@ export class LosslessAPI {
} }
async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) { async downloadTrack(id, quality = 'HI_RES_LOSSLESS', filename, options = {}) {
// Initialize taglib in the background.
initTagLib().catch(console.error);
const { onProgress, track } = options; const { onProgress, track } = options;
try { try {

View file

@ -98,6 +98,8 @@ let settingsModule = null;
let downloadsModule = null; let downloadsModule = null;
let metadataModule = null; let metadataModule = null;
export const managers = {};
async function loadSettingsModule() { async function loadSettingsModule() {
if (!settingsModule) { if (!settingsModule) {
settingsModule = await import('./settings.js'); settingsModule = await import('./settings.js');
@ -496,6 +498,7 @@ document.addEventListener('DOMContentLoaded', async () => {
window.monochromeScrobbler = scrobbler; window.monochromeScrobbler = scrobbler;
const lyricsManager = new LyricsManager(api); const lyricsManager = new LyricsManager(api);
ui.lyricsManager = lyricsManager; ui.lyricsManager = lyricsManager;
managers.lyricsManager = lyricsManager;
// Check browser support for local files // Check browser support for local files
const selectLocalBtn = document.getElementById('select-local-folder-btn'); const selectLocalBtn = document.getElementById('select-local-folder-btn');

View file

@ -17,6 +17,7 @@ import { DashDownloader } from './dash-downloader.js';
import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js'; import { generateM3U, generateM3U8, generateCUE, generateNFO, generateJSON } from './playlist-generator.js';
import { encodeToMp3 } from './mp3-encoder.js'; import { encodeToMp3 } from './mp3-encoder.js';
import { ffmpeg } from './ffmpeg.js'; import { ffmpeg } from './ffmpeg.js';
import { initTagLib } from './taglib.js';
const downloadTasks = new Map(); const downloadTasks = new Map();
const bulkDownloadTasks = new Map(); const bulkDownloadTasks = new Map();
@ -269,6 +270,9 @@ function removeBulkDownloadTask(notifEl) {
} }
async function downloadTrackBlob(track, quality, api, lyricsManager = null, signal = null) { async function downloadTrackBlob(track, quality, api, lyricsManager = null, signal = null) {
// Initialize taglib in the background.
initTagLib().catch(console.error);
let enrichedTrack = { let enrichedTrack = {
...track, ...track,
artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null), artist: track.artist || (track.artists && track.artists.length > 0 ? track.artists[0] : null),

File diff suppressed because it is too large Load diff

29
js/taglib.js Normal file
View file

@ -0,0 +1,29 @@
import { TagLib as _TagLib } from 'taglib-wasm';
/**
* @type {typeof import('taglib-wasm').TagLib}
*/
export const TagLib = _TagLib;
import TagLibWasm from '!/taglib-wasm/dist/taglib-web.wasm?url';
export { TagLibWasm };
let tagLib = null;
const wasmBinary = fetch(TagLibWasm).then((r) => r.arrayBuffer());
/**
*
* @returns {ReturnType<typeof TagLib.initialize>}
*/
export async function initTagLib() {
if (tagLib) return await tagLib;
tagLib = TagLib.initialize({
wasmBinary: await wasmBinary,
legacyMode: true,
});
console.log('TagLib initialized', { tagLib: await tagLib, TagLibWasm });
return await tagLib;
}

View file

@ -398,6 +398,9 @@ function resizeImageBlob(blob, size) {
/** /**
* Fetches and caches cover art as a Blob * Fetches and caches cover art as a Blob
* @param {Object} api - API instance with getCoverUrl method
* @param {string} coverId - ID of the cover art to fetch
* @returns {Promise<Blob|null>} - Cover art blob or null if not available
*/ */
export async function getCoverBlob(api, coverId) { export async function getCoverBlob(api, coverId) {
if (!coverId) return null; if (!coverId) return null;

View file

@ -30,6 +30,7 @@
"homepage": "https://github.com/SamidyFR/monochrome#readme", "homepage": "https://github.com/SamidyFR/monochrome#readme",
"devDependencies": { "devDependencies": {
"@neutralinojs/neu": "^11.7.0", "@neutralinojs/neu": "^11.7.0",
"@types/node": "^25.3.3",
"eslint": "^9.39.3", "eslint": "^9.39.3",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"globals": "^17.4.0", "globals": "^17.4.0",
@ -39,6 +40,7 @@
"stylelint": "^16.26.1", "stylelint": "^16.26.1",
"stylelint-config-standard": "^39.0.1", "stylelint-config-standard": "^39.0.1",
"stylelint-config-standard-scss": "^16.0.0", "stylelint-config-standard-scss": "^16.0.0",
"typescript": "^5.9.3",
"vite": "^7.3.1", "vite": "^7.3.1",
"vite-plugin-neutralino": "^1.0.3", "vite-plugin-neutralino": "^1.0.3",
"vite-plugin-pwa": "^1.2.0" "vite-plugin-pwa": "^1.2.0"
@ -60,6 +62,7 @@
"fuse.js": "^7.1.0", "fuse.js": "^7.1.0",
"jose": "^6.1.3", "jose": "^6.1.3",
"npm": "^11.11.0", "npm": "^11.11.0",
"pocketbase": "^0.26.8" "pocketbase": "^0.26.8",
"taglib-wasm": "^0.9.0"
} }
} }

View file

@ -10,6 +10,7 @@ export default defineConfig(({ mode }) => {
base: './', base: './',
resolve: { resolve: {
alias: { alias: {
'!': '/node_modules',
pocketbase: '/node_modules/pocketbase/dist/pocketbase.es.js', pocketbase: '/node_modules/pocketbase/dist/pocketbase.es.js',
}, },
}, },