taglib-wasm
Version:
TagLib for TypeScript platforms: Deno, Node.js, Bun, Electron, browsers, and Cloudflare Workers
236 lines (235 loc) • 7.11 kB
JavaScript
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
};