UNPKG

taglib-wasm

Version:

TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers

236 lines (235 loc) 7.11 kB
import { TagLib } from "./taglib.js"; import { readTags, updateTags } from "./simple.js"; function join(...paths) { return paths.filter((p) => p).join("/").replace(/\/+/g, "/"); } function extname(path) { const lastDot = path.lastIndexOf("."); if (lastDot === -1 || lastDot === path.length - 1) return ""; return path.slice(lastDot); } const DEFAULT_AUDIO_EXTENSIONS = [ ".mp3", ".m4a", ".mp4", ".flac", ".ogg", ".oga", ".opus", ".wav", ".wv", ".ape", ".mpc", ".tta", ".wma" ]; async function* walkDirectory(path, options = {}) { const { recursive = true, extensions = DEFAULT_AUDIO_EXTENSIONS } = options; if (typeof Deno !== "undefined") { for await (const entry of Deno.readDir(path)) { const fullPath = join(path, entry.name); if (entry.isDirectory && recursive) { yield* walkDirectory(fullPath, options); } else if (entry.isFile) { const ext = extname(entry.name).toLowerCase(); if (extensions.includes(ext)) { yield fullPath; } } } } else if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.node) { const fs = await import("fs/promises"); const entries = await fs.readdir(path, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(path, entry.name); if (entry.isDirectory() && recursive) { yield* walkDirectory(fullPath, options); } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase(); if (extensions.includes(ext)) { yield fullPath; } } } } else if (typeof globalThis.process !== "undefined" && globalThis.process.versions?.bun) { const fs = await import("fs/promises"); const entries = await fs.readdir(path, { withFileTypes: true }); for (const entry of entries) { const fullPath = join(path, entry.name); if (entry.isDirectory() && recursive) { yield* walkDirectory(fullPath, options); } else if (entry.isFile()) { const ext = extname(entry.name).toLowerCase(); if (extensions.includes(ext)) { yield fullPath; } } } } else { throw new Error("Directory scanning not supported in this runtime"); } } async function processBatch(files, processor, concurrency) { const results = []; for (let i = 0; i < files.length; i += concurrency) { const chunk = files.slice(i, i + concurrency); const chunkResults = await Promise.all( chunk.map((file) => processor(file)) ); results.push(...chunkResults); } return results; } async function scanFolder(folderPath, options = {}) { const startTime = Date.now(); const { maxFiles = Infinity, includeProperties = true, continueOnError = true, concurrency = 4, onProgress } = options; const files = []; const errors = []; const filePaths = []; let fileCount = 0; for await (const filePath of walkDirectory(folderPath, options)) { filePaths.push(filePath); fileCount++; if (fileCount >= maxFiles) break; } const totalFound = filePaths.length; let processed = 0; const taglib = await TagLib.initialize(); try { const processor = async (filePath) => { try { const tags = await readTags(filePath); let properties; if (includeProperties) { const audioFile = await taglib.open(filePath); try { const props = audioFile.audioProperties(); if (props) { properties = props; } } finally { audioFile.dispose(); } } processed++; onProgress?.(processed, totalFound, filePath); return { path: filePath, tags, properties }; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); if (continueOnError) { errors.push({ path: filePath, error: err }); processed++; onProgress?.(processed, totalFound, filePath); return { path: filePath, tags: {}, error: err }; } else { throw err; } } }; const batchSize = concurrency * 10; for (let i = 0; i < filePaths.length; i += batchSize) { const batch = filePaths.slice( i, Math.min(i + batchSize, filePaths.length) ); const batchResults = await processBatch(batch, processor, concurrency); files.push(...batchResults.filter((r) => !r.error)); } } finally { } return { files, errors, totalFound, totalProcessed: processed, duration: Date.now() - startTime }; } async function updateFolderTags(updates, options = {}) { const startTime = Date.now(); const { continueOnError = true, concurrency = 4 } = options; let successful = 0; const failed = []; const processor = async (update) => { try { await updateTags(update.path, update.tags); successful++; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); if (continueOnError) { failed.push({ path: update.path, error: err }); } else { throw err; } } }; const batchSize = concurrency * 10; for (let i = 0; i < updates.length; i += batchSize) { const batch = updates.slice(i, Math.min(i + batchSize, updates.length)); await processBatch( batch.map((u) => u.path), async (path) => { const update = batch.find((u) => u.path === path); await processor(update); return { path, tags: {} }; }, concurrency ); } return { successful, failed, duration: Date.now() - startTime }; } async function findDuplicates(folderPath, criteria = ["artist", "title"]) { const result = await scanFolder(folderPath); const duplicates = /* @__PURE__ */ new Map(); for (const file of result.files) { const key = criteria.map((field) => file.tags[field] || "").filter((v) => v !== "").join("|"); if (key) { const group = duplicates.get(key) || []; group.push(file); duplicates.set(key, group); } } for (const [key, files] of duplicates.entries()) { if (files.length < 2) { duplicates.delete(key); } } return duplicates; } async function exportFolderMetadata(folderPath, outputPath, options) { const result = await scanFolder(folderPath, options); const data = { folder: folderPath, scanDate: (/* @__PURE__ */ new Date()).toISOString(), summary: { totalFiles: result.totalFound, processedFiles: result.totalProcessed, errors: result.errors.length, duration: result.duration }, files: result.files, errors: result.errors }; if (typeof Deno !== "undefined") { await Deno.writeTextFile(outputPath, JSON.stringify(data, null, 2)); } else if (typeof globalThis.process !== "undefined") { const fs = await import("fs/promises"); await fs.writeFile(outputPath, JSON.stringify(data, null, 2)); } } export { exportFolderMetadata, findDuplicates, scanFolder, updateFolderTags };