taglib-wasm 2.2.3 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +192 -13
  2. package/dist/index.browser.d.ts +24 -4
  3. package/dist/index.browser.d.ts.map +1 -1
  4. package/dist/index.browser.js +1123 -208
  5. package/dist/index.d.ts +2 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +2 -0
  8. package/dist/simple.browser.js +527 -202
  9. package/dist/src/errors/base.d.ts +17 -2
  10. package/dist/src/errors/base.d.ts.map +1 -1
  11. package/dist/src/errors/base.js +18 -0
  12. package/dist/src/runtime/wasi-adapter/adapter.d.ts +13 -1
  13. package/dist/src/runtime/wasi-adapter/adapter.d.ts.map +1 -1
  14. package/dist/src/runtime/wasi-adapter/adapter.js +17 -0
  15. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts +4 -1
  16. package/dist/src/runtime/wasi-adapter/audio-properties.d.ts.map +1 -1
  17. package/dist/src/runtime/wasi-adapter/audio-properties.js +23 -6
  18. package/dist/src/runtime/wasi-adapter/path-open.d.ts +68 -0
  19. package/dist/src/runtime/wasi-adapter/path-open.d.ts.map +1 -0
  20. package/dist/src/runtime/wasi-adapter/path-open.js +76 -0
  21. package/dist/src/runtime/wasi-adapter/wasm-io.d.ts.map +1 -1
  22. package/dist/src/runtime/wasi-adapter/wasm-io.js +36 -28
  23. package/dist/src/runtime/wasi-host-loader.js +8 -3
  24. package/dist/src/runtime/wasmer-sdk-loader/types.d.ts +13 -0
  25. package/dist/src/runtime/wasmer-sdk-loader/types.d.ts.map +1 -1
  26. package/dist/src/simple/checksum-operations.d.ts +25 -0
  27. package/dist/src/simple/checksum-operations.d.ts.map +1 -0
  28. package/dist/src/simple/checksum-operations.js +7 -0
  29. package/dist/src/simple/index.d.ts +2 -0
  30. package/dist/src/simple/index.d.ts.map +1 -1
  31. package/dist/src/simple/index.js +2 -0
  32. package/dist/src/taglib/audio-file-checksum.d.ts +47 -0
  33. package/dist/src/taglib/audio-file-checksum.d.ts.map +1 -0
  34. package/dist/src/taglib/audio-file-checksum.js +77 -0
  35. package/dist/src/taglib/audio-file-impl.d.ts +4 -0
  36. package/dist/src/taglib/audio-file-impl.d.ts.map +1 -1
  37. package/dist/src/taglib/audio-file-impl.js +32 -0
  38. package/dist/src/taglib/audio-file-interface.d.ts +17 -0
  39. package/dist/src/taglib/audio-file-interface.d.ts.map +1 -1
  40. package/dist/src/taglib/media-ranges.d.ts +85 -0
  41. package/dist/src/taglib/media-ranges.d.ts.map +1 -0
  42. package/dist/src/taglib/media-ranges.js +192 -0
  43. package/dist/src/taglib/metadata-extent.d.ts +55 -0
  44. package/dist/src/taglib/metadata-extent.d.ts.map +1 -1
  45. package/dist/src/taglib/metadata-extent.js +24 -6
  46. package/dist/src/taglib/taglib-class.d.ts.map +1 -1
  47. package/dist/src/taglib/taglib-class.js +13 -24
  48. package/dist/src/utils/path.d.ts +12 -0
  49. package/dist/src/utils/path.d.ts.map +1 -1
  50. package/dist/src/utils/path.js +19 -0
  51. package/dist/src/version.d.ts +1 -1
  52. package/dist/src/version.js +1 -1
  53. package/dist/src/wasm.d.ts +9 -0
  54. package/dist/src/wasm.d.ts.map +1 -1
  55. package/dist/taglib-wasi.wasm +0 -0
  56. package/dist/taglib-web.wasm +0 -0
  57. package/dist/web.browser.d.ts +13 -0
  58. package/dist/web.browser.d.ts.map +1 -0
  59. package/dist/web.browser.js +3630 -0
  60. package/package.json +12 -4
@@ -157,6 +157,11 @@ var init_types2 = __esm({
157
157
  });
158
158
 
159
159
  // src/bwf/bext.ts
160
+ var bext_exports = {};
161
+ __export(bext_exports, {
162
+ decodeBext: () => decodeBext,
163
+ encodeBext: () => encodeBext
164
+ });
160
165
  function readFixedString(bytes, offset, len) {
161
166
  let end = offset;
162
167
  const max = Math.min(offset + len, bytes.length);
@@ -293,11 +298,29 @@ var init_base = __esm({
293
298
  "use strict";
294
299
  SUPPORTED_FORMATS = [
295
300
  "MP3",
301
+ "AAC",
296
302
  "MP4",
297
303
  "M4A",
298
304
  "FLAC",
299
305
  "OGG",
306
+ "OPUS",
307
+ "OggFLAC",
308
+ "SPEEX",
300
309
  "WAV",
310
+ "AIFF",
311
+ "ASF",
312
+ "APE",
313
+ "DSF",
314
+ "DSDIFF",
315
+ "WV",
316
+ "MPC",
317
+ "TTA",
318
+ "SHN",
319
+ "MOD",
320
+ "S3M",
321
+ "IT",
322
+ "XM",
323
+ "MATROSKA",
301
324
  "MKA"
302
325
  ];
303
326
  TagLibError = class _TagLibError extends Error {
@@ -1943,6 +1966,463 @@ var init_audio_file_base = __esm({
1943
1966
  }
1944
1967
  });
1945
1968
 
1969
+ // src/taglib/metadata-extent.ts
1970
+ function be32(b, offset) {
1971
+ return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
1972
+ }
1973
+ function startsWith(bytes, magic, at = 0) {
1974
+ if (bytes.length < at + magic.length) return false;
1975
+ for (let i = 0; i < magic.length; i++) {
1976
+ if (bytes[at + i] !== magic.charCodeAt(i)) return false;
1977
+ }
1978
+ return true;
1979
+ }
1980
+ function id3v2End(bytes) {
1981
+ if (!startsWith(bytes, "ID3") || bytes.length < 10) return void 0;
1982
+ const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
1983
+ const hasFooter = (bytes[5] & 16) !== 0;
1984
+ return 10 + size + (hasFooter ? 10 : 0);
1985
+ }
1986
+ function flacEnd(bytes, limit, start) {
1987
+ let offset = start + 4;
1988
+ for (; ; ) {
1989
+ if (offset + 4 > bytes.length) return void 0;
1990
+ const isLast = (bytes[offset] & 128) !== 0;
1991
+ const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
1992
+ offset += 4 + length;
1993
+ if (offset > limit) return offset;
1994
+ if (isLast) return offset;
1995
+ }
1996
+ }
1997
+ function flacMarkerOffset(bytes) {
1998
+ const at = startsWith(bytes, "ID3") ? id3v2End(bytes) : 0;
1999
+ if (at === void 0) return void 0;
2000
+ return startsWith(bytes, "fLaC", at) ? at : void 0;
2001
+ }
2002
+ function flacAudioStart(bytes) {
2003
+ const at = flacMarkerOffset(bytes);
2004
+ if (at === void 0) return void 0;
2005
+ return flacEnd(bytes, bytes.length, at);
2006
+ }
2007
+ function mp4MoovEnd(bytes, limit, start) {
2008
+ let offset = start;
2009
+ const readable = Math.min(bytes.length, limit);
2010
+ while (offset + 8 <= readable) {
2011
+ const size = be32(bytes, offset);
2012
+ const type = String.fromCharCode(
2013
+ bytes[offset + 4],
2014
+ bytes[offset + 5],
2015
+ bytes[offset + 6],
2016
+ bytes[offset + 7]
2017
+ );
2018
+ if (size < 8) return void 0;
2019
+ if (type === "moov") return offset + size;
2020
+ offset += size;
2021
+ }
2022
+ return void 0;
2023
+ }
2024
+ function oggMetadataEnd(bytes, limit, start) {
2025
+ let offset = start;
2026
+ let packetsCompleted = 0;
2027
+ let first = true;
2028
+ for (; ; ) {
2029
+ if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
2030
+ return void 0;
2031
+ }
2032
+ const segmentCount = bytes[offset + 26];
2033
+ const tableStart = offset + 27;
2034
+ if (tableStart + segmentCount > bytes.length) return void 0;
2035
+ let payload = 0;
2036
+ for (let i = 0; i < segmentCount; i++) {
2037
+ const segment = bytes[tableStart + i];
2038
+ payload += segment;
2039
+ if (segment < 255) packetsCompleted++;
2040
+ }
2041
+ if (first) {
2042
+ const payloadStart = tableStart + segmentCount;
2043
+ if (payloadStart + 5 > bytes.length) return void 0;
2044
+ if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
2045
+ return void 0;
2046
+ }
2047
+ first = false;
2048
+ }
2049
+ offset = tableStart + segmentCount + payload;
2050
+ if (packetsCompleted >= 2 || offset > limit) return offset;
2051
+ }
2052
+ }
2053
+ function mpegFrameLength(bytes, at) {
2054
+ if (bytes.length < at + 4) return 0;
2055
+ if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return 0;
2056
+ const version = bytes[at + 1] >> 3 & 3;
2057
+ const layer = bytes[at + 1] >> 1 & 3;
2058
+ if (version === 1 || layer === 0) return 0;
2059
+ const bitrateIndex = bytes[at + 2] >> 4 & 15;
2060
+ const sampleRateIndex = bytes[at + 2] >> 2 & 3;
2061
+ if (bitrateIndex === 0 || bitrateIndex === 15 || sampleRateIndex === 3) {
2062
+ return 0;
2063
+ }
2064
+ const mpeg1 = version === 3;
2065
+ const layerIndex = layer ^ 3;
2066
+ const bitrate = MPEG_BITRATES[mpeg1 ? 0 : 1][layerIndex][bitrateIndex];
2067
+ const sampleRateTable = mpeg1 ? 0 : version === 2 ? 1 : 2;
2068
+ const sampleRate = MPEG_SAMPLE_RATES[sampleRateTable][sampleRateIndex];
2069
+ const samplesPerFrame = layerIndex === 2 ? mpeg1 ? 1152 : 576 : layerIndex === 1 ? 1152 : 384;
2070
+ let frameLength = Math.floor(samplesPerFrame * bitrate * 125 / sampleRate);
2071
+ if ((bytes[at + 2] & 2) !== 0) frameLength += layerIndex === 0 ? 4 : 1;
2072
+ return frameLength === 0 ? 0 : frameLength;
2073
+ }
2074
+ function isMpegFrameSync(bytes, at = 0) {
2075
+ const frameLength = mpegFrameLength(bytes, at);
2076
+ if (frameLength === 0) return false;
2077
+ const next = at + frameLength;
2078
+ if (bytes.length < next + 4) return false;
2079
+ const mask = 4294839296;
2080
+ const header = ((bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3]) >>> 0 & mask) >>> 0;
2081
+ const nextHeader = ((bytes[next] << 24 | bytes[next + 1] << 16 | bytes[next + 2] << 8 | bytes[next + 3]) >>> 0 & mask) >>> 0;
2082
+ return header === nextHeader;
2083
+ }
2084
+ function containerEnd(bytes, at, limit) {
2085
+ if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2086
+ if (at + 8 > bytes.length) return void 0;
2087
+ if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2088
+ if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2089
+ if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2090
+ return null;
2091
+ }
2092
+ function metadataFitsInHeader(header, limit) {
2093
+ let end;
2094
+ if (startsWith(header, "ID3")) {
2095
+ const tagEnd = id3v2End(header);
2096
+ if (tagEnd === void 0) return false;
2097
+ end = tagEnd;
2098
+ if (tagEnd <= limit) {
2099
+ const behind = containerEnd(header, tagEnd, limit);
2100
+ if (behind === void 0) return false;
2101
+ if (behind !== null) end = behind;
2102
+ }
2103
+ } else {
2104
+ const container = containerEnd(header, 0, limit);
2105
+ if (container === null) {
2106
+ end = isMpegFrameSync(header) ? 0 : void 0;
2107
+ } else {
2108
+ end = container;
2109
+ }
2110
+ }
2111
+ if (end === void 0 || end > header.length) return false;
2112
+ return end <= limit;
2113
+ }
2114
+ function trailerFitsInFooter(tail, footerSize) {
2115
+ if (tail.length < Math.min(footerSize, 32)) return false;
2116
+ for (const offsetFromEnd of [32, 32 + 128]) {
2117
+ const at = tail.length - offsetFromEnd;
2118
+ if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2119
+ const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2120
+ const trailing = offsetFromEnd === 32 ? 0 : 128;
2121
+ if (size + 32 + trailing > footerSize) return false;
2122
+ }
2123
+ return true;
2124
+ }
2125
+ var MPEG_BITRATES, MPEG_SAMPLE_RATES;
2126
+ var init_metadata_extent = __esm({
2127
+ "src/taglib/metadata-extent.ts"() {
2128
+ "use strict";
2129
+ MPEG_BITRATES = [
2130
+ [
2131
+ [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
2132
+ // Layer I
2133
+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
2134
+ // Layer II
2135
+ [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]
2136
+ // Layer III
2137
+ ],
2138
+ [
2139
+ [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
2140
+ // Layer I
2141
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
2142
+ // Layer II
2143
+ [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
2144
+ // Layer III
2145
+ ]
2146
+ ];
2147
+ MPEG_SAMPLE_RATES = [
2148
+ [44100, 48e3, 32e3],
2149
+ // MPEG-1
2150
+ [22050, 24e3, 16e3],
2151
+ // MPEG-2
2152
+ [11025, 12e3, 8e3]
2153
+ // MPEG-2.5
2154
+ ];
2155
+ }
2156
+ });
2157
+
2158
+ // src/taglib/media-ranges.ts
2159
+ function adtsFrameLength(bytes, at) {
2160
+ if (bytes.length < at + 7) return 0;
2161
+ if (bytes[at] !== 255 || (bytes[at + 1] & 246) !== 240) return 0;
2162
+ const length = (bytes[at + 3] & 3) << 11 | bytes[at + 4] << 3 | bytes[at + 5] >> 5;
2163
+ return length >= 7 ? length : 0;
2164
+ }
2165
+ function trailingTagStart(bytes, end = bytes.length) {
2166
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2167
+ let at = Math.max(0, Math.min(end, bytes.length));
2168
+ for (; ; ) {
2169
+ if (at >= 128 && text(bytes, at - 128, 3) === "TAG") {
2170
+ at -= 128;
2171
+ continue;
2172
+ }
2173
+ if (at >= 32 && text(bytes, at - 32, 8) === "APETAGEX") {
2174
+ const size = view.getUint32(at - 32 + 12, true);
2175
+ const flags = view.getUint32(at - 32 + 20, true);
2176
+ const start = at - size - ((flags & 2147483648) !== 0 ? 32 : 0);
2177
+ if (start < 0 || start >= at) return at;
2178
+ at = start;
2179
+ continue;
2180
+ }
2181
+ return at;
2182
+ }
2183
+ }
2184
+ function firstFrame(bytes) {
2185
+ const from = id3v2End(bytes) ?? 0;
2186
+ const limit = Math.min(bytes.length - 6, from + MPEG_SCAN_LIMIT);
2187
+ for (let o = from; o < limit; o++) {
2188
+ const len = frameLengthAt(bytes, o);
2189
+ if (len > 0 && frameLengthAt(bytes, o + len) > 0) return o;
2190
+ }
2191
+ return -1;
2192
+ }
2193
+ function forwardLastFrame(bytes, from, end) {
2194
+ let o = from;
2195
+ let start = -1;
2196
+ let length = 0;
2197
+ let frames = 0;
2198
+ while (o + 6 <= end) {
2199
+ const len = frameLengthAt(bytes, o);
2200
+ if (len === 0 || o + len > end) break;
2201
+ start = o;
2202
+ length = len;
2203
+ frames++;
2204
+ o += len;
2205
+ }
2206
+ return { start, length, frames };
2207
+ }
2208
+ function walkMpeg(bytes) {
2209
+ const from = firstFrame(bytes);
2210
+ if (from < 0) return fallback("no valid first frame");
2211
+ const end = trailingTagStart(bytes);
2212
+ const last = forwardLastFrame(bytes, from, end);
2213
+ if (last.start < 0) return fallback("no valid frames");
2214
+ return {
2215
+ kind: "ranges",
2216
+ ranges: [{ offset: from, length: last.start + last.length - from }],
2217
+ detail: `first=${from} last=${last.start}+${last.length} frames=${last.frames}`
2218
+ };
2219
+ }
2220
+ function flacStreamInfoMd5(bytes) {
2221
+ const marker = flacMarkerOffset(bytes);
2222
+ if (marker === void 0) return void 0;
2223
+ const header = marker + 4;
2224
+ if (header + 4 > bytes.length) return void 0;
2225
+ if ((bytes[header] & 127) !== 0) return void 0;
2226
+ const blockLength = bytes[header + 1] << 16 | bytes[header + 2] << 8 | bytes[header + 3];
2227
+ if (blockLength < 34) return void 0;
2228
+ const payload = header + 4;
2229
+ if (payload + 34 > bytes.length) return void 0;
2230
+ let hex = "";
2231
+ for (const byte of bytes.subarray(payload + 18, payload + 34)) {
2232
+ hex += byte.toString(16).padStart(2, "0");
2233
+ }
2234
+ return hex;
2235
+ }
2236
+ function walkFlac(bytes) {
2237
+ const start = flacAudioStart(bytes);
2238
+ if (start === void 0) return fallback("no fLaC marker");
2239
+ const end = trailingTagStart(bytes);
2240
+ if (end <= start) return fallback("no audio bytes");
2241
+ return {
2242
+ kind: "ranges",
2243
+ ranges: [{ offset: start, length: end - start }],
2244
+ detail: `audioStart=${start} end=${end}`
2245
+ };
2246
+ }
2247
+ function atomLength(bytes, view, o) {
2248
+ const size = view.getUint32(o, false);
2249
+ if (size === 0) return { length: bytes.length - o, headerSize: 8 };
2250
+ if (size === 1) {
2251
+ if (o + 16 > bytes.length) return void 0;
2252
+ const big = view.getBigUint64(o + 8, false);
2253
+ if (big > BigInt(Number.MAX_SAFE_INTEGER)) return void 0;
2254
+ return { length: Number(big), headerSize: 16 };
2255
+ }
2256
+ return { length: size, headerSize: 8 };
2257
+ }
2258
+ function walkMp4(bytes) {
2259
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2260
+ const ranges = [];
2261
+ let o = 0;
2262
+ while (o + 8 <= bytes.length) {
2263
+ const type = text(bytes, o + 4, 4);
2264
+ const atom = atomLength(bytes, view, o);
2265
+ if (atom === void 0) return fallback(`malformed ${type} size`);
2266
+ if (atom.length < atom.headerSize) {
2267
+ return fallback(`malformed ${type} size`);
2268
+ }
2269
+ if (o + atom.length > bytes.length) return fallback(`oversized ${type}`);
2270
+ if (type === "mdat" && atom.length > atom.headerSize) {
2271
+ ranges.push({
2272
+ offset: o + atom.headerSize,
2273
+ length: atom.length - atom.headerSize
2274
+ });
2275
+ }
2276
+ o += atom.length;
2277
+ }
2278
+ if (o !== bytes.length) return fallback("trailing bytes after the last atom");
2279
+ if (ranges.length === 0) return fallback("no non-empty mdat");
2280
+ return { kind: "ranges", ranges, detail: `${ranges.length} mdat(s)` };
2281
+ }
2282
+ function walkWav(bytes) {
2283
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
2284
+ if (text(bytes, 0, 4) !== "RIFF") return fallback("not RIFF");
2285
+ if (text(bytes, 8, 4) !== "WAVE") return fallback("not WAVE");
2286
+ const ranges = [];
2287
+ let o = 12;
2288
+ while (o + 8 <= bytes.length) {
2289
+ const id = text(bytes, o, 4);
2290
+ const size = view.getUint32(o + 4, true);
2291
+ const dataStart = o + 8;
2292
+ if (dataStart + size > bytes.length) return fallback(`truncated ${id}`);
2293
+ if (id === "data" && size > 0) {
2294
+ ranges.push({ offset: dataStart, length: size });
2295
+ }
2296
+ o = dataStart + size + size % 2;
2297
+ }
2298
+ if (ranges.length === 0) return fallback("no data chunk");
2299
+ return { kind: "ranges", ranges, detail: `${ranges.length} data chunk(s)` };
2300
+ }
2301
+ function mediaRanges(format, bytes) {
2302
+ const walked = (() => {
2303
+ switch (format.toUpperCase()) {
2304
+ case "MP3":
2305
+ case "AAC":
2306
+ return walkMpeg(bytes);
2307
+ case "FLAC":
2308
+ return walkFlac(bytes);
2309
+ case "MP4":
2310
+ case "M4A":
2311
+ return walkMp4(bytes);
2312
+ case "WAV":
2313
+ return walkWav(bytes);
2314
+ default:
2315
+ return { kind: "fallback", ranges: [], detail: "format not covered" };
2316
+ }
2317
+ })();
2318
+ if (walked.kind === "fallback") {
2319
+ return {
2320
+ kind: "fallback",
2321
+ ranges: [{ offset: 0, length: bytes.length }],
2322
+ detail: `${walked.detail} \u2192 whole file`
2323
+ };
2324
+ }
2325
+ return walked;
2326
+ }
2327
+ var decoder, text, fallback, frameLengthAt, MPEG_SCAN_LIMIT;
2328
+ var init_media_ranges = __esm({
2329
+ "src/taglib/media-ranges.ts"() {
2330
+ "use strict";
2331
+ init_metadata_extent();
2332
+ decoder = new TextDecoder();
2333
+ text = (b, o, n) => decoder.decode(b.subarray(o, o + n));
2334
+ fallback = (detail) => ({
2335
+ kind: "fallback",
2336
+ ranges: [],
2337
+ detail
2338
+ });
2339
+ frameLengthAt = (bytes, at) => mpegFrameLength(bytes, at) || adtsFrameLength(bytes, at);
2340
+ MPEG_SCAN_LIMIT = 65536;
2341
+ }
2342
+ });
2343
+
2344
+ // src/taglib/audio-file-checksum.ts
2345
+ async function fileBytes(source, io) {
2346
+ if (source.path) return await io.readFile(source.path);
2347
+ if (source.blob) return new Uint8Array(await source.blob.arrayBuffer());
2348
+ if (!source.partiallyLoaded && source.bytes.length > 0) return source.bytes;
2349
+ throw new MetadataError(
2350
+ "read",
2351
+ "the handle holds no file bytes and has no readable source; a partial handle that has been saved drops its source \u2014 checksum the saved path instead"
2352
+ );
2353
+ }
2354
+ function joinRanges(bytes, ranges) {
2355
+ const total = ranges.reduce((n, r) => n + r.length, 0);
2356
+ const overrun = ranges.find((r) => r.offset + r.length > bytes.length);
2357
+ if (overrun || total > bytes.length) {
2358
+ throw new MetadataError(
2359
+ "read",
2360
+ `range walk overran the buffer (${overrun ? `range ${overrun.offset}+${overrun.length}` : `${total} bytes`} past the ${bytes.length}-byte buffer)`
2361
+ );
2362
+ }
2363
+ if (ranges.length === 1) {
2364
+ const only = ranges[0];
2365
+ const view = bytes.subarray(only.offset, only.offset + only.length);
2366
+ return view.buffer instanceof ArrayBuffer ? view : new Uint8Array(view);
2367
+ }
2368
+ const out = new Uint8Array(total);
2369
+ let at = 0;
2370
+ for (const r of ranges) {
2371
+ out.set(bytes.subarray(r.offset, r.offset + r.length), at);
2372
+ at += r.length;
2373
+ }
2374
+ return out;
2375
+ }
2376
+ async function mediaChecksum(source, format, io, options = {}) {
2377
+ if (options.basis === "pcm") {
2378
+ if (format.toUpperCase() !== "FLAC") {
2379
+ throw new UnsupportedFormatError(format, ["FLAC"], {
2380
+ operation: "media checksum (basis: pcm)"
2381
+ });
2382
+ }
2383
+ const windowed = source.bytes.length > 0 ? source.bytes : source.path && io.readPartial ? await io.readPartial(source.path, STREAMINFO_WINDOW, 0) : void 0;
2384
+ let md5 = windowed === void 0 ? void 0 : flacStreamInfoMd5(windowed);
2385
+ if (md5 === void 0 && windowed !== source.bytes) {
2386
+ md5 = flacStreamInfoMd5(await fileBytes(source, io));
2387
+ }
2388
+ if (md5 === void 0) {
2389
+ throw new MetadataError(
2390
+ "read",
2391
+ "STREAMINFO digest unavailable"
2392
+ );
2393
+ }
2394
+ return {
2395
+ source: "flac-streaminfo-md5",
2396
+ algorithm: "md5",
2397
+ hex: md5,
2398
+ bytesHashed: 16
2399
+ };
2400
+ }
2401
+ const bytes = await fileBytes(source, io);
2402
+ const walk = mediaRanges(format, bytes);
2403
+ const payload = joinRanges(bytes, walk.ranges);
2404
+ const digest = new Uint8Array(
2405
+ await crypto.subtle.digest("SHA-256", payload)
2406
+ );
2407
+ let hex = "";
2408
+ for (const byte of digest) hex += byte.toString(16).padStart(2, "0");
2409
+ return {
2410
+ source: walk.kind === "ranges" ? "audio-payload" : "file",
2411
+ algorithm: "sha256",
2412
+ hex,
2413
+ bytesHashed: payload.length
2414
+ };
2415
+ }
2416
+ var STREAMINFO_WINDOW;
2417
+ var init_audio_file_checksum = __esm({
2418
+ "src/taglib/audio-file-checksum.ts"() {
2419
+ "use strict";
2420
+ init_media_ranges();
2421
+ init_errors2();
2422
+ STREAMINFO_WINDOW = 65536;
2423
+ }
2424
+ });
2425
+
1946
2426
  // src/utils/file.ts
1947
2427
  async function readFileData(file) {
1948
2428
  if (file instanceof Uint8Array) return file;
@@ -2050,8 +2530,8 @@ function wrapEmbindHandle(raw) {
2050
2530
  // Text only — language/description are not surfaced by the PropertyMap.
2051
2531
  getLyrics() {
2052
2532
  const props = raw.getProperties();
2053
- return (props["LYRICS"] ?? []).map((text) => ({
2054
- text,
2533
+ return (props["LYRICS"] ?? []).map((text2) => ({
2534
+ text: text2,
2055
2535
  description: "",
2056
2536
  language: ""
2057
2537
  }));
@@ -2304,6 +2784,8 @@ var init_audio_file_impl = __esm({
2304
2784
  init_rating();
2305
2785
  init_detector();
2306
2786
  init_audio_file_base();
2787
+ init_audio_file_checksum();
2788
+ init_platform_io_browser_stub();
2307
2789
  init_save_reconstruct();
2308
2790
  AudioFileImpl = class extends BaseAudioFileImpl {
2309
2791
  constructor(module, fileHandle, sourcePath, originalSource, isPartiallyLoaded = false, partialLoadOptions) {
@@ -2316,6 +2798,9 @@ var init_audio_file_impl = __esm({
2316
2798
  partialLoadOptions
2317
2799
  );
2318
2800
  __publicField(this, "pathModeBuffer", null);
2801
+ /** How the handle was loaded — `isPartiallyLoaded` is mutated by saveToFile(). */
2802
+ __publicField(this, "partiallyLoadedAtConstruction");
2803
+ this.partiallyLoadedAtConstruction = isPartiallyLoaded;
2319
2804
  }
2320
2805
  save() {
2321
2806
  if (this.isPartiallyLoaded && this.originalSource) {
@@ -2351,6 +2836,31 @@ var init_audio_file_impl = __esm({
2351
2836
  "No file data available: in-memory buffer is empty and no source path is set"
2352
2837
  );
2353
2838
  }
2839
+ mediaChecksum(options) {
2840
+ return mediaChecksum(
2841
+ {
2842
+ // The cheap, no-IO accessor — `getFileBuffer()` would read the whole
2843
+ // file from disk in path mode (and cache it), which the source rule is
2844
+ // about to read for itself.
2845
+ bytes: this.handle.getBuffer(),
2846
+ // empty in path mode: the data is on disk
2847
+ // Both optional fields are conditional: they are `undefined` far more
2848
+ // often than not, and `exactOptionalPropertyTypes` refuses an explicit
2849
+ // `undefined` on an optional property.
2850
+ ...this.sourcePath !== void 0 ? { path: this.sourcePath } : {},
2851
+ // originalSource is the File when the caller passed one. A partial File
2852
+ // has no path, so this is the only way back to the real bytes.
2853
+ ...this.originalSource instanceof Blob ? { blob: this.originalSource } : {},
2854
+ // Captured at construction, NOT this.isPartiallyLoaded: saveToFile() sets
2855
+ // that flag false (and clears originalSource) while the handle still holds
2856
+ // the spliced image, which would make the source rule hash the image.
2857
+ partiallyLoaded: this.partiallyLoadedAtConstruction
2858
+ },
2859
+ this.getFormat(),
2860
+ getPlatformIO(),
2861
+ options
2862
+ );
2863
+ }
2354
2864
  async saveToFile(path) {
2355
2865
  const targetPath = path ?? this.sourcePath;
2356
2866
  if (!targetPath) {
@@ -2548,181 +3058,6 @@ var init_audio_file_impl = __esm({
2548
3058
  }
2549
3059
  });
2550
3060
 
2551
- // src/taglib/metadata-extent.ts
2552
- function be32(b, offset) {
2553
- return (b[offset] << 24 | b[offset + 1] << 16 | b[offset + 2] << 8 | b[offset + 3]) >>> 0;
2554
- }
2555
- function startsWith(bytes, magic, at = 0) {
2556
- if (bytes.length < at + magic.length) return false;
2557
- for (let i = 0; i < magic.length; i++) {
2558
- if (bytes[at + i] !== magic.charCodeAt(i)) return false;
2559
- }
2560
- return true;
2561
- }
2562
- function id3v2End(bytes) {
2563
- if (bytes.length < 10) return void 0;
2564
- const size = bytes[6] << 21 | bytes[7] << 14 | bytes[8] << 7 | bytes[9];
2565
- const hasFooter = (bytes[5] & 16) !== 0;
2566
- return 10 + size + (hasFooter ? 10 : 0);
2567
- }
2568
- function flacEnd(bytes, limit, start) {
2569
- let offset = start + 4;
2570
- for (; ; ) {
2571
- if (offset + 4 > bytes.length) return void 0;
2572
- const isLast = (bytes[offset] & 128) !== 0;
2573
- const length = bytes[offset + 1] << 16 | bytes[offset + 2] << 8 | bytes[offset + 3];
2574
- offset += 4 + length;
2575
- if (offset > limit) return offset;
2576
- if (isLast) return offset;
2577
- }
2578
- }
2579
- function mp4MoovEnd(bytes, limit, start) {
2580
- let offset = start;
2581
- const readable = Math.min(bytes.length, limit);
2582
- while (offset + 8 <= readable) {
2583
- const size = be32(bytes, offset);
2584
- const type = String.fromCharCode(
2585
- bytes[offset + 4],
2586
- bytes[offset + 5],
2587
- bytes[offset + 6],
2588
- bytes[offset + 7]
2589
- );
2590
- if (size < 8) return void 0;
2591
- if (type === "moov") return offset + size;
2592
- offset += size;
2593
- }
2594
- return void 0;
2595
- }
2596
- function oggMetadataEnd(bytes, limit, start) {
2597
- let offset = start;
2598
- let packetsCompleted = 0;
2599
- let first = true;
2600
- for (; ; ) {
2601
- if (offset + 27 > bytes.length || !startsWith(bytes, "OggS", offset)) {
2602
- return void 0;
2603
- }
2604
- const segmentCount = bytes[offset + 26];
2605
- const tableStart = offset + 27;
2606
- if (tableStart + segmentCount > bytes.length) return void 0;
2607
- let payload = 0;
2608
- for (let i = 0; i < segmentCount; i++) {
2609
- const segment = bytes[tableStart + i];
2610
- payload += segment;
2611
- if (segment < 255) packetsCompleted++;
2612
- }
2613
- if (first) {
2614
- const payloadStart = tableStart + segmentCount;
2615
- if (payloadStart + 5 > bytes.length) return void 0;
2616
- if (bytes[payloadStart] === 127 && startsWith(bytes, "FLAC", payloadStart + 1)) {
2617
- return void 0;
2618
- }
2619
- first = false;
2620
- }
2621
- offset = tableStart + segmentCount + payload;
2622
- if (packetsCompleted >= 2 || offset > limit) return offset;
2623
- }
2624
- }
2625
- function isMpegFrameSync(bytes, at = 0) {
2626
- if (bytes.length < at + 4) return false;
2627
- if (bytes[at] !== 255 || (bytes[at + 1] & 224) !== 224) return false;
2628
- const version = bytes[at + 1] >> 3 & 3;
2629
- const layer = bytes[at + 1] >> 1 & 3;
2630
- if (version === 1 || layer === 0) return false;
2631
- const bitrateIndex = bytes[at + 2] >> 4 & 15;
2632
- const sampleRateIndex = bytes[at + 2] >> 2 & 3;
2633
- if (bitrateIndex === 0 || bitrateIndex === 15 || sampleRateIndex === 3) {
2634
- return false;
2635
- }
2636
- const mpeg1 = version === 3;
2637
- const layerIndex = layer ^ 3;
2638
- const bitrate = MPEG_BITRATES[mpeg1 ? 0 : 1][layerIndex][bitrateIndex];
2639
- const sampleRateTable = mpeg1 ? 0 : version === 2 ? 1 : 2;
2640
- const sampleRate = MPEG_SAMPLE_RATES[sampleRateTable][sampleRateIndex];
2641
- const samplesPerFrame = layerIndex === 2 ? mpeg1 ? 1152 : 576 : layerIndex === 1 ? 1152 : 384;
2642
- let frameLength = Math.floor(samplesPerFrame * bitrate * 125 / sampleRate);
2643
- if ((bytes[at + 2] & 2) !== 0) frameLength += layerIndex === 0 ? 4 : 1;
2644
- if (frameLength === 0) return false;
2645
- const next = at + frameLength;
2646
- if (bytes.length < next + 4) return false;
2647
- const mask = 4294839296;
2648
- const header = ((bytes[at] << 24 | bytes[at + 1] << 16 | bytes[at + 2] << 8 | bytes[at + 3]) >>> 0 & mask) >>> 0;
2649
- const nextHeader = ((bytes[next] << 24 | bytes[next + 1] << 16 | bytes[next + 2] << 8 | bytes[next + 3]) >>> 0 & mask) >>> 0;
2650
- return header === nextHeader;
2651
- }
2652
- function containerEnd(bytes, at, limit) {
2653
- if (at >= bytes.length) return bytes.length < limit ? null : void 0;
2654
- if (at + 8 > bytes.length) return void 0;
2655
- if (startsWith(bytes, "fLaC", at)) return flacEnd(bytes, limit, at);
2656
- if (startsWith(bytes, "ftyp", at + 4)) return mp4MoovEnd(bytes, limit, at);
2657
- if (startsWith(bytes, "OggS", at)) return oggMetadataEnd(bytes, limit, at);
2658
- return null;
2659
- }
2660
- function metadataFitsInHeader(header, limit) {
2661
- let end;
2662
- if (startsWith(header, "ID3")) {
2663
- const tagEnd = id3v2End(header);
2664
- if (tagEnd === void 0) return false;
2665
- end = tagEnd;
2666
- if (tagEnd <= limit) {
2667
- const behind = containerEnd(header, tagEnd, limit);
2668
- if (behind === void 0) return false;
2669
- if (behind !== null) end = behind;
2670
- }
2671
- } else {
2672
- const container = containerEnd(header, 0, limit);
2673
- if (container === null) {
2674
- end = isMpegFrameSync(header) ? 0 : void 0;
2675
- } else {
2676
- end = container;
2677
- }
2678
- }
2679
- if (end === void 0 || end > header.length) return false;
2680
- return end <= limit;
2681
- }
2682
- function trailerFitsInFooter(tail, footerSize) {
2683
- if (tail.length < Math.min(footerSize, 32)) return false;
2684
- for (const offsetFromEnd of [32, 32 + 128]) {
2685
- const at = tail.length - offsetFromEnd;
2686
- if (at < 0 || !startsWith(tail, "APETAGEX", at)) continue;
2687
- const size = (tail[at + 12] | tail[at + 13] << 8 | tail[at + 14] << 16 | tail[at + 15] << 24) >>> 0;
2688
- const trailing = offsetFromEnd === 32 ? 0 : 128;
2689
- if (size + 32 + trailing > footerSize) return false;
2690
- }
2691
- return true;
2692
- }
2693
- var MPEG_BITRATES, MPEG_SAMPLE_RATES;
2694
- var init_metadata_extent = __esm({
2695
- "src/taglib/metadata-extent.ts"() {
2696
- "use strict";
2697
- MPEG_BITRATES = [
2698
- [
2699
- [0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0],
2700
- // Layer I
2701
- [0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0],
2702
- // Layer II
2703
- [0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0]
2704
- // Layer III
2705
- ],
2706
- [
2707
- [0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0],
2708
- // Layer I
2709
- [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0],
2710
- // Layer II
2711
- [0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0]
2712
- // Layer III
2713
- ]
2714
- ];
2715
- MPEG_SAMPLE_RATES = [
2716
- [44100, 48e3, 32e3],
2717
- // MPEG-1
2718
- [22050, 24e3, 16e3],
2719
- // MPEG-2
2720
- [11025, 12e3, 8e3]
2721
- // MPEG-2.5
2722
- ];
2723
- }
2724
- });
2725
-
2726
3061
  // src/taglib/load-audio-data.ts
2727
3062
  async function loadAudioData(input, opts) {
2728
3063
  if (opts.partial && typeof File !== "undefined" && input instanceof File) {
@@ -3009,7 +3344,7 @@ var VERSION;
3009
3344
  var init_version = __esm({
3010
3345
  "src/version.ts"() {
3011
3346
  "use strict";
3012
- VERSION = "2.2.3";
3347
+ VERSION = "2.3.0";
3013
3348
  }
3014
3349
  });
3015
3350
 
@@ -3132,30 +3467,19 @@ var init_taglib_class = __esm({
3132
3467
  const actualInput = isNamedAudioInput(input) ? input.data : input;
3133
3468
  const sourcePath = typeof actualInput === "string" ? actualInput : void 0;
3134
3469
  if (typeof actualInput === "string" && this.module.isWasi) {
3135
- const fileHandle2 = this.module.createFileHandle();
3136
- try {
3137
- const fh = fileHandle2;
3138
- if (fh.loadFromPath) {
3139
- const wasiPath = toWasiPath(actualInput);
3140
- const success = fh.loadFromPath(wasiPath);
3141
- if (!success) {
3142
- throw new InvalidFormatError(
3143
- `Failed to load audio file. Path: ${actualInput}`
3144
- );
3145
- }
3146
- return new AudioFileImpl(
3147
- this.module,
3148
- fileHandle2,
3149
- sourcePath,
3150
- actualInput,
3151
- false
3152
- );
3153
- }
3154
- } catch (error) {
3155
- if (typeof fileHandle2.destroy === "function") {
3156
- fileHandle2.destroy();
3157
- }
3158
- throw error;
3470
+ const wasiPath = toWasiPath(actualInput);
3471
+ if (this.module.loadAudioPath) {
3472
+ const fileHandle2 = await this.module.loadAudioPath(
3473
+ wasiPath,
3474
+ actualInput
3475
+ );
3476
+ return new AudioFileImpl(
3477
+ this.module,
3478
+ fileHandle2,
3479
+ sourcePath,
3480
+ actualInput,
3481
+ false
3482
+ );
3159
3483
  }
3160
3484
  }
3161
3485
  const opts = {
@@ -3308,6 +3632,11 @@ init_taglib2();
3308
3632
  init_audio_formats();
3309
3633
  init_errors2();
3310
3634
 
3635
+ // src/runtime/deno-detect.ts
3636
+ function isDenoCompiled() {
3637
+ return typeof Deno !== "undefined" && typeof Deno.mainModule === "string" && Deno.mainModule.includes("deno-compile://");
3638
+ }
3639
+
3311
3640
  // src/simple/config.ts
3312
3641
  var cachedTagLib = null;
3313
3642
  var bufferModeEnabled = false;
@@ -3499,7 +3828,6 @@ async function readPictureMetadata(file) {
3499
3828
  // src/simple/batch-operations.ts
3500
3829
  init_audio_formats();
3501
3830
  init_errors2();
3502
- init_properties();
3503
3831
  init_tag_mapping();
3504
3832
  async function executeBatch(files, options, processor) {
3505
3833
  if (files.length === 0) return { items: [], duration: 0 };
@@ -3618,11 +3946,10 @@ async function readMetadataBatch(files, options = {}) {
3618
3946
  });
3619
3947
  }
3620
3948
 
3621
- // src/simple/index.ts
3622
- init_types2();
3623
-
3624
- // src/constants/index.ts
3625
- init_properties();
3949
+ // src/simple/checksum-operations.ts
3950
+ async function readMediaChecksum(file, options) {
3951
+ return withAudioFile(file, (audioFile) => audioFile.mediaChecksum(options));
3952
+ }
3626
3953
 
3627
3954
  // src/constants/format-mappings.ts
3628
3955
  var FormatMappings = {
@@ -3703,6 +4030,588 @@ function propertyValue(props, key) {
3703
4030
  return props[key]?.[0];
3704
4031
  }
3705
4032
 
4033
+ // src/folder-api/folder-disc.ts
4034
+ function dirname(path) {
4035
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
4036
+ return i === -1 ? path : path.slice(0, i);
4037
+ }
4038
+ var UNCONDITIONAL_MARKERS = [
4039
+ "cd",
4040
+ "disc",
4041
+ "disk",
4042
+ "dvd",
4043
+ "sacd",
4044
+ "blu-ray",
4045
+ "bd",
4046
+ "digital media"
4047
+ ];
4048
+ var GATED_MARKERS = ["tape", "vinyl", "cassette", "lp", "record"];
4049
+ var MARKERS = [...UNCONDITIONAL_MARKERS, ...GATED_MARKERS];
4050
+ var GATED_MARKER_SET = new Set(GATED_MARKERS);
4051
+ var WORD_NUMBERS = {
4052
+ one: 1,
4053
+ two: 2,
4054
+ three: 3,
4055
+ four: 4,
4056
+ five: 5,
4057
+ six: 6,
4058
+ seven: 7,
4059
+ eight: 8,
4060
+ nine: 9,
4061
+ ten: 10,
4062
+ eleven: 11,
4063
+ twelve: 12
4064
+ };
4065
+ function romanToInt(s) {
4066
+ const values = {
4067
+ i: 1,
4068
+ v: 5,
4069
+ x: 10,
4070
+ l: 50,
4071
+ c: 100,
4072
+ d: 500,
4073
+ m: 1e3
4074
+ };
4075
+ let total = 0;
4076
+ let prev = 0;
4077
+ for (let i = s.length - 1; i >= 0; i--) {
4078
+ const v = values[s[i]];
4079
+ if (v === void 0) return NaN;
4080
+ total += v < prev ? -v : v;
4081
+ prev = v;
4082
+ }
4083
+ return total;
4084
+ }
4085
+ function parseNumberToken(token) {
4086
+ if (/^\d+$/.test(token)) return parseInt(token, 10);
4087
+ const word = WORD_NUMBERS[token.toLowerCase()];
4088
+ if (word) return word;
4089
+ const roman = romanToInt(token.toLowerCase());
4090
+ return Number.isFinite(roman) ? roman : void 0;
4091
+ }
4092
+ var SEP = String.raw`[\s._#-]*`;
4093
+ var NUM = String.raw`(?:\d+|(?:[ivxlcdm]+|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|[a-z])(?![a-z]))`;
4094
+ function parseDiscName(name) {
4095
+ const trimmed = name.trim();
4096
+ if (/^(?:extras?|bonus)$/i.test(trimmed)) return void 0;
4097
+ for (const marker of MARKERS) {
4098
+ const left = String.raw`(?:^|[^a-z0-9])`;
4099
+ const esc = marker.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
4100
+ const re = new RegExp(
4101
+ left + String.raw`(${esc})(?![a-z])${SEP}(${NUM})`,
4102
+ "i"
4103
+ );
4104
+ const m = trimmed.match(re);
4105
+ if (!m) continue;
4106
+ const sideLetter = /^[a-z]$/i.test(m[2]);
4107
+ const number = sideLetter ? void 0 : parseNumberToken(m[2]);
4108
+ if (number === void 0 && !sideLetter) continue;
4109
+ if (!sideLetter && /^[ivxlcdm]{2,}$/i.test(m[2]) && (number ?? 0) > 100) continue;
4110
+ const isGated = GATED_MARKER_SET.has(marker.toLowerCase()) || sideLetter;
4111
+ const markerStart = m.index + m[1].length - marker.length;
4112
+ const rawPrefix = trimmed.slice(0, markerStart).trim();
4113
+ const title = rawPrefix.replace(/[()[\]]/g, "").replace(/[\s._#-]+$/, "") || void 0;
4114
+ const isExact = title === void 0;
4115
+ const after = trimmed.slice(m.index + m[0].length).trim();
4116
+ const discTitle = after.replace(/^[()[\],:;\-–—\s]+/, "").replace(/[)\]\[,:;\-–—\s]+$/, "").trim() || void 0;
4117
+ const ofMatch = after.trim().match(/^of\s+(\d+)\b/i);
4118
+ const total = ofMatch ? parseInt(ofMatch[1], 10) : void 0;
4119
+ return {
4120
+ kind: isExact ? "exact" : "embedded",
4121
+ number: sideLetter ? void 0 : number,
4122
+ total,
4123
+ title,
4124
+ discTitle,
4125
+ gated: isGated,
4126
+ sideLetter,
4127
+ digit: /^\d+$/.test(m[2])
4128
+ };
4129
+ }
4130
+ const volume = trimmed.match(
4131
+ /^(?:volume|vol\.?|part|pt\.?)\s*(\d+)(?:\s*of\s*(\d+))?$/i
4132
+ );
4133
+ if (volume) {
4134
+ return {
4135
+ kind: "volume",
4136
+ number: parseInt(volume[1], 10),
4137
+ total: volume[2] ? parseInt(volume[2], 10) : void 0,
4138
+ title: void 0,
4139
+ discTitle: void 0,
4140
+ gated: false,
4141
+ sideLetter: false,
4142
+ digit: true
4143
+ };
4144
+ }
4145
+ if (/^bonus\s+disc$/i.test(trimmed)) {
4146
+ return {
4147
+ kind: "bonus",
4148
+ number: void 0,
4149
+ total: void 0,
4150
+ title: void 0,
4151
+ discTitle: void 0,
4152
+ gated: true,
4153
+ sideLetter: false,
4154
+ digit: false
4155
+ };
4156
+ }
4157
+ if (/^(?:\d+|v\d+|p\d+|[ab])$/i.test(trimmed)) {
4158
+ return {
4159
+ kind: "bare",
4160
+ number: /^\d+$/i.test(trimmed) ? parseInt(trimmed, 10) : void 0,
4161
+ total: void 0,
4162
+ title: void 0,
4163
+ discTitle: void 0,
4164
+ gated: true,
4165
+ sideLetter: false,
4166
+ digit: false
4167
+ };
4168
+ }
4169
+ return void 0;
4170
+ }
4171
+ function baseConfidence(parse) {
4172
+ switch (parse.kind) {
4173
+ case "exact":
4174
+ return parse.digit ? "high" : "low";
4175
+ case "embedded":
4176
+ return "medium";
4177
+ case "volume":
4178
+ return "medium";
4179
+ case "bonus":
4180
+ case "bare":
4181
+ return "low";
4182
+ }
4183
+ }
4184
+ function discFolderInfo(name) {
4185
+ const parse = parseDiscName(name);
4186
+ if (!parse) return void 0;
4187
+ return {
4188
+ kind: parse.kind,
4189
+ gated: parse.gated,
4190
+ number: parse.number,
4191
+ total: parse.total,
4192
+ title: parse.title,
4193
+ discTitle: parse.discTitle,
4194
+ confidence: parse.gated ? "low" : baseConfidence(parse)
4195
+ };
4196
+ }
4197
+
4198
+ // src/utils/path.ts
4199
+ function basename(path) {
4200
+ const i = Math.max(path.lastIndexOf("/"), path.lastIndexOf("\\"));
4201
+ return i === -1 ? path : path.slice(i + 1);
4202
+ }
4203
+
4204
+ // src/folder-api/album-corroborate.ts
4205
+ var CONFIDENCE_RANK = {
4206
+ low: 0,
4207
+ medium: 1,
4208
+ high: 2
4209
+ };
4210
+ function corroborated(parse, siblings) {
4211
+ if (!parse.gated && parse.kind === "exact") return true;
4212
+ if (siblings.length === 0) return false;
4213
+ const siblingParses = siblings.map((s) => parseDiscName(basename(s))).filter((p) => p !== void 0);
4214
+ const hasExactSibling = siblingParses.some(
4215
+ (p) => !p.gated && p.kind === "exact"
4216
+ );
4217
+ const hasNumberedSibling = siblingParses.some(
4218
+ (p) => p.number !== void 0
4219
+ );
4220
+ if (parse.number !== void 0 && hasNumberedSibling) return true;
4221
+ if (!parse.gated && parse.kind === "embedded" && hasExactSibling) return true;
4222
+ if (!parse.gated && parse.kind === "volume" && hasNumberedSibling) {
4223
+ return true;
4224
+ }
4225
+ if (parse.sideLetter) {
4226
+ return siblingParses.some((p) => p.sideLetter);
4227
+ }
4228
+ if (parse.kind === "bonus" || parse.kind === "bare") {
4229
+ const numberedCount = siblingParses.filter((p) => p.number !== void 0).length;
4230
+ if (numberedCount >= 2) return true;
4231
+ if (parse.number !== void 0 && hasNumberedSibling) return true;
4232
+ }
4233
+ return false;
4234
+ }
4235
+
4236
+ // src/folder-api/album-identity.ts
4237
+ function normalizeKey(s) {
4238
+ return s.trim().toLowerCase().replace(/\s+/g, " ");
4239
+ }
4240
+ function isGenericAlbumArtist(albumArtist) {
4241
+ if (!albumArtist || albumArtist.trim() === "") return true;
4242
+ const n = normalizeKey(albumArtist);
4243
+ return n === "various artists" || n === "va";
4244
+ }
4245
+ function evidenceFor(item, confirmedDiscs, flatSubdivisions2) {
4246
+ const dir = dirname(item.path);
4247
+ const own = confirmedDiscs.get(dir);
4248
+ if (own) {
4249
+ return {
4250
+ folderDiscNumber: own.parse.number,
4251
+ total: own.parse.total,
4252
+ folderDiscTitle: own.parse.discTitle,
4253
+ confidence: own.confidence,
4254
+ albumDir: dirname(dir)
4255
+ };
4256
+ }
4257
+ let cursor = dirname(dir);
4258
+ while (cursor !== void 0) {
4259
+ const entry = confirmedDiscs.get(cursor);
4260
+ if (entry) {
4261
+ return {
4262
+ folderDiscNumber: entry.parse.number,
4263
+ total: entry.parse.total,
4264
+ folderDiscTitle: entry.parse.discTitle,
4265
+ confidence: entry.confidence,
4266
+ albumDir: dirname(cursor)
4267
+ };
4268
+ }
4269
+ const parent = dirname(cursor);
4270
+ if (parent === cursor) break;
4271
+ cursor = parent;
4272
+ }
4273
+ const flat = flatSubdivisions2.get(dir)?.get(item.path);
4274
+ if (flat !== void 0) {
4275
+ return {
4276
+ folderDiscNumber: flat,
4277
+ total: void 0,
4278
+ folderDiscTitle: void 0,
4279
+ confidence: "medium",
4280
+ albumDir: dir
4281
+ };
4282
+ }
4283
+ return void 0;
4284
+ }
4285
+ function folderIdentity(dir, confirmedDiscs, scanRoot) {
4286
+ const own = confirmedDiscs.get(dir);
4287
+ if (!own) {
4288
+ const title2 = basename(dir);
4289
+ return title2 ? { title: title2, titleSource: dir } : void 0;
4290
+ }
4291
+ if (own.parse.title !== void 0) {
4292
+ return { title: own.parse.title, titleSource: dirname(dir) };
4293
+ }
4294
+ const source = dirname(dir);
4295
+ if (source === scanRoot || source === dir) return void 0;
4296
+ const title = basename(source);
4297
+ return title ? { title, titleSource: source } : void 0;
4298
+ }
4299
+ function buildAlbumIdentity(okItems, confirmedDiscs, flatSubdivisions2, options) {
4300
+ const albums = /* @__PURE__ */ new Map();
4301
+ const unmatched = [];
4302
+ for (const item of okItems) {
4303
+ const dir = dirname(item.path);
4304
+ const albumTag = item.tags.album?.[0]?.trim();
4305
+ const albumArtistTag = item.tags.albumArtist?.[0]?.trim();
4306
+ if (albumTag) {
4307
+ const artistPart = isGenericAlbumArtist(albumArtistTag) ? "" : normalizeKey(albumArtistTag ?? "");
4308
+ const key2 = `${artistPart}::${normalizeKey(albumTag)}`;
4309
+ let acc2 = albums.get(key2);
4310
+ if (!acc2) {
4311
+ acc2 = {
4312
+ key: key2,
4313
+ album: albumTag,
4314
+ albumArtist: isGenericAlbumArtist(albumArtistTag) ? void 0 : albumArtistTag,
4315
+ source: "tags",
4316
+ directory: void 0,
4317
+ items: [],
4318
+ evidence: /* @__PURE__ */ new Map()
4319
+ };
4320
+ albums.set(key2, acc2);
4321
+ }
4322
+ const evidence2 = evidenceFor(item, confirmedDiscs, flatSubdivisions2);
4323
+ acc2.evidence.set(item.path, evidence2);
4324
+ acc2.items.push({
4325
+ ...item.metadata,
4326
+ albumDir: evidence2?.albumDir ?? dir,
4327
+ discNumber: evidence2?.folderDiscNumber
4328
+ });
4329
+ continue;
4330
+ }
4331
+ if (!options.useFolderFallback) {
4332
+ unmatched.push(item.metadata);
4333
+ continue;
4334
+ }
4335
+ const identity = folderIdentity(dir, confirmedDiscs, options.scanRoot);
4336
+ if (!identity) {
4337
+ unmatched.push(item.metadata);
4338
+ continue;
4339
+ }
4340
+ const key = `${identity.titleSource}::${normalizeKey(identity.title)}`;
4341
+ let acc = albums.get(key);
4342
+ if (!acc) {
4343
+ acc = {
4344
+ key,
4345
+ album: identity.title,
4346
+ albumArtist: void 0,
4347
+ source: "folder",
4348
+ directory: identity.titleSource,
4349
+ items: [],
4350
+ evidence: /* @__PURE__ */ new Map()
4351
+ };
4352
+ albums.set(key, acc);
4353
+ }
4354
+ const evidence = evidenceFor(item, confirmedDiscs, flatSubdivisions2);
4355
+ acc.evidence.set(item.path, evidence);
4356
+ acc.items.push({
4357
+ ...item.metadata,
4358
+ albumDir: evidence?.albumDir ?? dir,
4359
+ discNumber: evidence?.folderDiscNumber
4360
+ });
4361
+ }
4362
+ for (const [key, acc] of [...albums]) {
4363
+ if (acc.source === "tags") continue;
4364
+ for (const other of albums.values()) {
4365
+ if (other === acc || other.source !== "tags") continue;
4366
+ if (normalizeKey(other.album) !== normalizeKey(acc.album)) continue;
4367
+ const albumDir = acc.directory;
4368
+ const sharesChain = albumDir !== void 0 && other.items.some((o) => {
4369
+ const od = dirname(o.path);
4370
+ return od === albumDir || od.startsWith(albumDir + "/");
4371
+ });
4372
+ if (!sharesChain) continue;
4373
+ for (const item of acc.items) {
4374
+ other.items.push(item);
4375
+ other.evidence.set(item.path, acc.evidence.get(item.path));
4376
+ }
4377
+ albums.delete(key);
4378
+ break;
4379
+ }
4380
+ }
4381
+ return { albums, unmatched };
4382
+ }
4383
+
4384
+ // src/folder-api/album-discs.ts
4385
+ function assembleDiscs(albums) {
4386
+ const groups = [];
4387
+ for (const acc of albums.values()) {
4388
+ const dirTagCommon = /* @__PURE__ */ new Map();
4389
+ const resolvedByFile = /* @__PURE__ */ new Map();
4390
+ for (const item of acc.items) {
4391
+ const dir = dirname(item.path);
4392
+ if (!dirTagCommon.has(dir)) {
4393
+ const tagged = acc.items.filter((i) => dirname(i.path) === dir).map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
4394
+ dirTagCommon.set(
4395
+ dir,
4396
+ tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0
4397
+ );
4398
+ }
4399
+ const evidence = acc.evidence.get(item.path);
4400
+ const tagCommon = dirTagCommon.get(dir);
4401
+ resolvedByFile.set(
4402
+ item.path,
4403
+ tagCommon ?? evidence?.folderDiscNumber
4404
+ );
4405
+ }
4406
+ const discs = /* @__PURE__ */ new Map();
4407
+ const order = [];
4408
+ const noEvidenceItems = [];
4409
+ for (const item of acc.items) {
4410
+ const evidence = acc.evidence.get(item.path);
4411
+ if (evidence === void 0) {
4412
+ noEvidenceItems.push(item);
4413
+ continue;
4414
+ }
4415
+ const resolved = resolvedByFile.get(item.path);
4416
+ const discKey = resolved === void 0 ? `dir:${dirname(item.path)}` : String(resolved);
4417
+ let disc = discs.get(discKey);
4418
+ if (!disc) {
4419
+ disc = {
4420
+ discNumber: resolved,
4421
+ totalDiscs: evidence.total,
4422
+ folderDiscNumber: evidence.folderDiscNumber,
4423
+ folderDiscTitle: evidence.folderDiscTitle,
4424
+ tagDiscNumber: void 0,
4425
+ confidence: evidence.confidence,
4426
+ items: []
4427
+ };
4428
+ discs.set(discKey, disc);
4429
+ order.push(discKey);
4430
+ }
4431
+ disc.items.push(item);
4432
+ }
4433
+ if (noEvidenceItems.length > 0) {
4434
+ const numbered = [...discs.values()].filter((d) => d.folderDiscNumber !== void 0).sort((a, b) => a.folderDiscNumber - b.folderDiscNumber);
4435
+ if (numbered.length > 0) {
4436
+ numbered[0].items.push(...noEvidenceItems);
4437
+ } else {
4438
+ const key = "dir:__none__";
4439
+ let disc = discs.get(key);
4440
+ if (!disc) {
4441
+ disc = {
4442
+ discNumber: void 0,
4443
+ totalDiscs: void 0,
4444
+ folderDiscNumber: void 0,
4445
+ folderDiscTitle: void 0,
4446
+ tagDiscNumber: void 0,
4447
+ confidence: "high",
4448
+ items: []
4449
+ };
4450
+ discs.set(key, disc);
4451
+ order.push(key);
4452
+ }
4453
+ disc.items.push(...noEvidenceItems);
4454
+ }
4455
+ }
4456
+ for (const disc of discs.values()) {
4457
+ const tagged = disc.items.map((i) => i.tags.discNumber).filter((d) => d !== void 0 && d > 0);
4458
+ const common = tagged.length > 0 && tagged.every((d) => d === tagged[0]) ? tagged[0] : void 0;
4459
+ disc.tagDiscNumber = common;
4460
+ disc.discNumber = common ?? disc.folderDiscNumber;
4461
+ for (const item of disc.items) item.discNumber = disc.discNumber;
4462
+ }
4463
+ const albumTaggedTotals = acc.items.map((i) => i.tags.totalDiscs).filter((t) => t !== void 0 && t > 0);
4464
+ const commonTotal = albumTaggedTotals.length > 0 && albumTaggedTotals.every((t) => t === albumTaggedTotals[0]) ? albumTaggedTotals[0] : void 0;
4465
+ const ofN = [...discs.values()].map((d) => d.totalDiscs).find(
4466
+ (t) => t !== void 0
4467
+ );
4468
+ const maxSibling = [...discs.values()].map((d) => d.discNumber).filter((n) => n !== void 0).reduce((a, b) => Math.max(a, b), 0) || void 0;
4469
+ for (const disc of discs.values()) {
4470
+ disc.totalDiscs = commonTotal ?? ofN ?? maxSibling;
4471
+ }
4472
+ const sortedDiscs = [...discs.values()].sort((a, b) => {
4473
+ const an = a.discNumber === void 0 ? -1 : a.discNumber;
4474
+ const bn = b.discNumber === void 0 ? -1 : b.discNumber;
4475
+ if (an !== bn) return an - bn;
4476
+ return (a.folderDiscTitle ?? "").localeCompare(b.folderDiscTitle ?? "");
4477
+ });
4478
+ for (const disc of sortedDiscs) {
4479
+ disc.items.sort((a, b) => {
4480
+ const at = a.tags.track ?? Infinity;
4481
+ const bt = b.tags.track ?? Infinity;
4482
+ if (at !== bt) return at - bt;
4483
+ return a.path.localeCompare(b.path);
4484
+ });
4485
+ }
4486
+ const flags = acc.items.map((i) => i.tags.compilation).filter((c) => c !== void 0);
4487
+ const compilation = flags.length > 0 && flags.every((f) => f === flags[0]) ? flags[0] : void 0;
4488
+ const albumDirs = new Set(acc.items.map((i) => i.albumDir));
4489
+ const directory = albumDirs.size === 1 ? [...albumDirs][0] : acc.directory;
4490
+ groups.push({
4491
+ key: acc.key,
4492
+ albumArtist: acc.albumArtist,
4493
+ album: acc.album,
4494
+ source: acc.source,
4495
+ compilation,
4496
+ directory,
4497
+ discs: sortedDiscs,
4498
+ items: sortedDiscs.flatMap((d) => d.items)
4499
+ });
4500
+ }
4501
+ return groups;
4502
+ }
4503
+
4504
+ // src/folder-api/album-grouping.ts
4505
+ function flatPrefix(filename) {
4506
+ const separated = filename.match(/^(\d{1,2})[\s._-](\d{1,2})/);
4507
+ if (separated) return { disc: parseInt(separated[1], 10), form: "separated" };
4508
+ const compact = filename.match(/^(\d)(\d{2})(?:\D|$)/);
4509
+ if (compact) return { disc: parseInt(compact[1], 10), form: "compact" };
4510
+ return void 0;
4511
+ }
4512
+ function normalizeOk(result) {
4513
+ const okItems = [];
4514
+ const errors = [];
4515
+ for (const item of result.items) {
4516
+ if (item.status === "ok") {
4517
+ okItems.push({ path: item.path, tags: item.tags, metadata: item });
4518
+ } else {
4519
+ errors.push({ path: item.path, error: item.error });
4520
+ }
4521
+ }
4522
+ return { okItems, errors };
4523
+ }
4524
+ function buildTree(okItems) {
4525
+ const tree = /* @__PURE__ */ new Map();
4526
+ for (const item of okItems) {
4527
+ const dir = dirname(item.path);
4528
+ let node = tree.get(dir);
4529
+ if (!node) {
4530
+ node = { files: [], parent: dirname(dir) };
4531
+ tree.set(dir, node);
4532
+ }
4533
+ node.files.push(item);
4534
+ }
4535
+ return tree;
4536
+ }
4537
+ function confirmDiscs(tree, minRank) {
4538
+ const confirmedDiscs = /* @__PURE__ */ new Map();
4539
+ for (const [dir, node] of tree) {
4540
+ if (node.files.length === 0) continue;
4541
+ const parse = parseDiscName(basename(dir));
4542
+ if (!parse) continue;
4543
+ const siblings = [...tree.keys()].filter(
4544
+ (d) => d !== dir && dirname(d) === dirname(dir)
4545
+ );
4546
+ const corroboratedBySiblings = corroborated(parse, siblings);
4547
+ let confidence = parse.gated ? "low" : baseConfidence(parse);
4548
+ if (parse.gated) {
4549
+ if (!corroboratedBySiblings) continue;
4550
+ } else if (parse.kind === "bonus" || parse.kind === "bare") {
4551
+ if (!corroboratedBySiblings) continue;
4552
+ } else if (corroboratedBySiblings) {
4553
+ confidence = "high";
4554
+ }
4555
+ if (CONFIDENCE_RANK[confidence] < minRank) continue;
4556
+ confirmedDiscs.set(dir, { parse, confidence });
4557
+ }
4558
+ return confirmedDiscs;
4559
+ }
4560
+ function flatSubdivisions(tree, confirmedDiscs, useFlatPrefixes) {
4561
+ const flat = /* @__PURE__ */ new Map();
4562
+ if (!useFlatPrefixes) return flat;
4563
+ for (const [dir, node] of tree) {
4564
+ if (node.files.length === 0 || confirmedDiscs.has(dir)) continue;
4565
+ const byFile = /* @__PURE__ */ new Map();
4566
+ const forms = /* @__PURE__ */ new Set();
4567
+ for (const item of node.files) {
4568
+ const prefix = flatPrefix(basename(item.path));
4569
+ if (prefix) {
4570
+ byFile.set(item.path, prefix.disc);
4571
+ forms.add(prefix.form);
4572
+ }
4573
+ }
4574
+ if (byFile.size < 2) continue;
4575
+ const distinct = [...new Set(byFile.values())].sort((a, b) => a - b);
4576
+ const allCompact = forms.size === 1 && forms.has("compact");
4577
+ const plausible = allCompact ? distinct.every((d) => d >= 1 && d <= distinct.length) : true;
4578
+ if (plausible) flat.set(dir, byFile);
4579
+ }
4580
+ return flat;
4581
+ }
4582
+ function groupAlbums(result, options = {}) {
4583
+ const minRank = CONFIDENCE_RANK[options.minFolderConfidence ?? "low"];
4584
+ const useFlatPrefixes = options.flatDiscPrefixes ?? true;
4585
+ const useFolderFallback = options.folderFallback ?? true;
4586
+ const { okItems, errors } = normalizeOk(result);
4587
+ if (okItems.length === 0) {
4588
+ return { albums: [], singles: [], unmatched: [], errors };
4589
+ }
4590
+ const tree = buildTree(okItems);
4591
+ const confirmedDiscs = confirmDiscs(tree, minRank);
4592
+ const flat = flatSubdivisions(tree, confirmedDiscs, useFlatPrefixes);
4593
+ const { albums, unmatched } = buildAlbumIdentity(
4594
+ okItems,
4595
+ confirmedDiscs,
4596
+ flat,
4597
+ { scanRoot: options.scanRoot, useFolderFallback }
4598
+ );
4599
+ const groups = assembleDiscs(albums);
4600
+ const albumsOut = [];
4601
+ const singles = [];
4602
+ for (const group of groups) {
4603
+ if (group.items.length === 1) {
4604
+ singles.push(group.items[0]);
4605
+ } else {
4606
+ albumsOut.push(group);
4607
+ }
4608
+ }
4609
+ albumsOut.sort(
4610
+ (a, b) => (a.albumArtist ?? "").localeCompare(b.albumArtist ?? "") || (a.album ?? "").localeCompare(b.album ?? "")
4611
+ );
4612
+ return { albums: albumsOut, singles, unmatched, errors };
4613
+ }
4614
+
3706
4615
  // src/web-utils/data-url.ts
3707
4616
  init_classes();
3708
4617
  function pictureToDataURL(picture) {
@@ -3841,6 +4750,7 @@ async function createPictureGallery(file, container, options = {}) {
3841
4750
 
3842
4751
  // index.browser.ts
3843
4752
  init_types2();
4753
+ init_bext();
3844
4754
  init_rating();
3845
4755
  init_module_loader_browser();
3846
4756
  export {
@@ -3867,6 +4777,7 @@ export {
3867
4777
  applyPictures,
3868
4778
  applyTags,
3869
4779
  applyTagsToFile,
4780
+ bext_exports as bwf,
3870
4781
  canvasToPicture,
3871
4782
  clearPictures,
3872
4783
  clearTags,
@@ -3874,13 +4785,16 @@ export {
3874
4785
  createPictureGallery,
3875
4786
  createTagLib,
3876
4787
  dataURLToPicture,
4788
+ discFolderInfo,
3877
4789
  displayPicture,
3878
4790
  findPictureByType,
3879
4791
  getAllProperties,
3880
4792
  getAllPropertyKeys,
3881
4793
  getPropertiesByFormat,
3882
4794
  getPropertyMetadata,
4795
+ groupAlbums,
3883
4796
  imageFileToPicture,
4797
+ isDenoCompiled,
3884
4798
  isEnvironmentError,
3885
4799
  isFileOperationError,
3886
4800
  isInvalidFormatError,
@@ -3897,6 +4811,7 @@ export {
3897
4811
  propertyValues,
3898
4812
  readCoverArt,
3899
4813
  readFormat,
4814
+ readMediaChecksum,
3900
4815
  readMetadata,
3901
4816
  readMetadataBatch,
3902
4817
  readPictureMetadata,