@storyteller-platform/audiobook 0.3.3 → 0.3.5

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.
@@ -0,0 +1,50 @@
1
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
2
+ var __typeError = (msg) => {
3
+ throw TypeError(msg);
4
+ };
5
+ var __using = (stack, value, async) => {
6
+ if (value != null) {
7
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
8
+ var dispose, inner;
9
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
10
+ if (dispose === void 0) {
11
+ dispose = value[__knownSymbol("dispose")];
12
+ if (async) inner = dispose;
13
+ }
14
+ if (typeof dispose !== "function") __typeError("Object not disposable");
15
+ if (inner) dispose = function() {
16
+ try {
17
+ inner.call(this);
18
+ } catch (e) {
19
+ return Promise.reject(e);
20
+ }
21
+ };
22
+ stack.push([async, dispose, value]);
23
+ } else if (async) {
24
+ stack.push([async]);
25
+ }
26
+ return value;
27
+ };
28
+ var __callDispose = (stack, error, hasError) => {
29
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
30
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
31
+ };
32
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
33
+ var next = (it) => {
34
+ while (it = stack.pop()) {
35
+ try {
36
+ var result = it[1] && it[1].call(it[2]);
37
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
38
+ } catch (e) {
39
+ fail(e);
40
+ }
41
+ }
42
+ if (hasError) throw error;
43
+ };
44
+ return next();
45
+ };
46
+
47
+ export {
48
+ __using,
49
+ __callDispose
50
+ };
package/dist/entry.cjs CHANGED
@@ -119,8 +119,13 @@ class AudiobookEntry {
119
119
  const info = await this.getInfo();
120
120
  return info.duration;
121
121
  }
122
+ async getBitRate() {
123
+ const info = await this.getInfo();
124
+ return info.bitRate;
125
+ }
122
126
  async getResource() {
123
127
  const duration = await this.getDuration();
128
+ const bitrate = await this.getBitRate() ?? null;
124
129
  const title = await this.getTrackTitle() ?? null;
125
130
  const type = (0, import_mime.lookupAudioMime)(this.filename);
126
131
  return {
@@ -128,7 +133,7 @@ class AudiobookEntry {
128
133
  title,
129
134
  type,
130
135
  duration,
131
- bitrate: null
136
+ bitrate
132
137
  };
133
138
  }
134
139
  async getChapters() {
@@ -139,9 +144,28 @@ class AudiobookEntry {
139
144
  start: chapter.startTime
140
145
  }));
141
146
  }
147
+ async setChapters(chapters) {
148
+ const info = await this.getInfo();
149
+ const duration = await this.getDuration();
150
+ const ffmpegChapters = [];
151
+ for (let i = 0; i < ffmpegChapters.length; i++) {
152
+ const chapter = chapters[i];
153
+ const prevFfmpegChapter = ffmpegChapters[i - 1];
154
+ ffmpegChapters.push({
155
+ id: i,
156
+ startTime: chapter.start ?? 0,
157
+ title: chapter.title ?? `ch${i}`,
158
+ endTime: duration
159
+ });
160
+ if (prevFfmpegChapter) {
161
+ prevFfmpegChapter.endTime = chapter.start ?? 0;
162
+ }
163
+ }
164
+ info.chapters = ffmpegChapters;
165
+ }
142
166
  async saveAndClose() {
143
167
  const info = await this.getInfo();
144
- await (0, import_ffmpeg.writeTrackMetadata)(this.filename, info.tags, info.attachedPic);
168
+ await (0, import_ffmpeg.writeTrackMetadata)(this.filename, info);
145
169
  }
146
170
  }
147
171
  // Annotate the CommonJS export names for ESM import in node:
package/dist/entry.d.cts CHANGED
@@ -30,8 +30,10 @@ declare class AudiobookEntry {
30
30
  getReleaseDate(): Promise<Date | null>;
31
31
  setReleaseDate(date: Date): Promise<void>;
32
32
  getDuration(): Promise<number>;
33
+ getBitRate(): Promise<number | undefined>;
33
34
  getResource(): Promise<AudiobookResource>;
34
35
  getChapters(): Promise<AudiobookChapter[]>;
36
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
35
37
  saveAndClose(): Promise<void>;
36
38
  }
37
39
 
package/dist/entry.d.ts CHANGED
@@ -30,8 +30,10 @@ declare class AudiobookEntry {
30
30
  getReleaseDate(): Promise<Date | null>;
31
31
  setReleaseDate(date: Date): Promise<void>;
32
32
  getDuration(): Promise<number>;
33
+ getBitRate(): Promise<number | undefined>;
33
34
  getResource(): Promise<AudiobookResource>;
34
35
  getChapters(): Promise<AudiobookChapter[]>;
36
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
35
37
  saveAndClose(): Promise<void>;
36
38
  }
37
39
 
package/dist/entry.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./chunk-BIEQXUOY.js";
1
2
  import {
2
3
  getTrackMetadata,
3
4
  writeTrackMetadata
@@ -99,8 +100,13 @@ class AudiobookEntry {
99
100
  const info = await this.getInfo();
100
101
  return info.duration;
101
102
  }
103
+ async getBitRate() {
104
+ const info = await this.getInfo();
105
+ return info.bitRate;
106
+ }
102
107
  async getResource() {
103
108
  const duration = await this.getDuration();
109
+ const bitrate = await this.getBitRate() ?? null;
104
110
  const title = await this.getTrackTitle() ?? null;
105
111
  const type = lookupAudioMime(this.filename);
106
112
  return {
@@ -108,7 +114,7 @@ class AudiobookEntry {
108
114
  title,
109
115
  type,
110
116
  duration,
111
- bitrate: null
117
+ bitrate
112
118
  };
113
119
  }
114
120
  async getChapters() {
@@ -119,9 +125,28 @@ class AudiobookEntry {
119
125
  start: chapter.startTime
120
126
  }));
121
127
  }
128
+ async setChapters(chapters) {
129
+ const info = await this.getInfo();
130
+ const duration = await this.getDuration();
131
+ const ffmpegChapters = [];
132
+ for (let i = 0; i < ffmpegChapters.length; i++) {
133
+ const chapter = chapters[i];
134
+ const prevFfmpegChapter = ffmpegChapters[i - 1];
135
+ ffmpegChapters.push({
136
+ id: i,
137
+ startTime: chapter.start ?? 0,
138
+ title: chapter.title ?? `ch${i}`,
139
+ endTime: duration
140
+ });
141
+ if (prevFfmpegChapter) {
142
+ prevFfmpegChapter.endTime = chapter.start ?? 0;
143
+ }
144
+ }
145
+ info.chapters = ffmpegChapters;
146
+ }
122
147
  async saveAndClose() {
123
148
  const info = await this.getInfo();
124
- await writeTrackMetadata(this.filename, info.tags, info.attachedPic);
149
+ await writeTrackMetadata(this.filename, info);
125
150
  }
126
151
  }
127
152
  export {
package/dist/ffmpeg.cjs CHANGED
@@ -94,9 +94,15 @@ async function getTrackMetadata(path) {
94
94
  const stdout = await execCmd(
95
95
  `ffprobe -i ${quotePath(path)} -v quiet -show_format -show_chapters -show_streams -output_format json`
96
96
  );
97
- const { chapters, streams, format, duration, bit_rate } = JSON.parse(
98
- stdout
99
- );
97
+ const { chapters, streams, format } = JSON.parse(stdout);
98
+ let id3v2Version = null;
99
+ if ((0, import_path.extname)(path).toLowerCase() === ".mp3") {
100
+ const { stderr: id3v2VersionOut } = await execPromise(
101
+ `ffprobe -i ${quotePath(path)} -v debug`
102
+ );
103
+ const id3v2VersionMatch = id3v2VersionOut.match(/id3v2 ver:(3|4)/);
104
+ id3v2Version = (id3v2VersionMatch == null ? void 0 : id3v2VersionMatch[1]) ?? null;
105
+ }
100
106
  const attachedPicStream = streams.find(
101
107
  (stream) => !!stream.disposition.attached_pic
102
108
  );
@@ -123,8 +129,9 @@ async function getTrackMetadata(path) {
123
129
  ])
124
130
  };
125
131
  return {
126
- duration: parseFloat(duration),
127
- bitRate: bit_rate !== void 0 ? parseFloat(bit_rate) : bit_rate,
132
+ id3v2Version,
133
+ duration: parseFloat(format.duration),
134
+ bitRate: format.bit_rate !== void 0 ? parseFloat(format.bit_rate) : format.bit_rate,
128
135
  tags: {
129
136
  title: format.tags.title ?? format.tags.Title,
130
137
  subtitle: format.tags.subtitle ?? format.tags.Subtitle,
@@ -149,9 +156,16 @@ async function getTrackMetadata(path) {
149
156
  attachedPic
150
157
  };
151
158
  }
152
- async function writeTrackMetadata(path, metadata, attachedPic) {
159
+ function escapeFfmetadata(str) {
160
+ return str.replaceAll(/\\/g, "\\\\").replaceAll(/=/g, "\\=").replaceAll(/;/g, "\\;").replaceAll(/#/g, "\\#").replaceAll(/\n/g, "\\n");
161
+ }
162
+ async function writeTrackMetadata(path, trackInfo) {
163
+ const { tags: metadata, id3v2Version, chapters, attachedPic } = trackInfo;
153
164
  const args = [];
154
165
  const metadataArgs = [];
166
+ if (id3v2Version) {
167
+ metadataArgs.push(`-id3v2_version ${id3v2Version}`);
168
+ }
155
169
  if (metadata.title) {
156
170
  metadataArgs.push(`-metadata title="${escapeQuotes(metadata.title)}"`);
157
171
  }
@@ -193,13 +207,34 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
193
207
  `-metadata publisher="${escapeQuotes(metadata.publisher)}"`
194
208
  );
195
209
  }
210
+ metadataArgs.push("-map 0:a -map_metadata 0");
196
211
  const ext = (0, import_path.extname)(path);
197
212
  const tmpPath = (0, import_node_path.join)(
198
213
  (0, import_node_os.tmpdir)(),
199
214
  `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}${ext}`
200
215
  );
216
+ const chapterFilePath = (0, import_node_path.join)(
217
+ (0, import_node_os.tmpdir)(),
218
+ `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}.txt`
219
+ );
201
220
  let picPath = null;
202
221
  try {
222
+ let chapterFileContents = ";FFMETADATA1\n\n";
223
+ for (const chapter of chapters) {
224
+ chapterFileContents += `[CHAPTER]
225
+ TIMEBASE=1/1000
226
+ START=${Math.floor(chapter.startTime * 1e3)}
227
+ END=${Math.floor(chapter.endTime * 1e3)}
228
+ `;
229
+ if (chapter.title) {
230
+ chapterFileContents += `TITLE=${escapeFfmetadata(chapter.title)}
231
+ `;
232
+ }
233
+ chapterFileContents += "\n";
234
+ }
235
+ await (0, import_promises.writeFile)(chapterFilePath, chapterFileContents);
236
+ args.push(`-i ${chapterFilePath}`);
237
+ metadataArgs.push(`-map_chapters 1`);
203
238
  if (attachedPic) {
204
239
  const imageExt = attachedPic.mimeType.split("/")[1];
205
240
  picPath = (0, import_node_path.join)(
@@ -207,9 +242,9 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
207
242
  `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}.${imageExt}`
208
243
  );
209
244
  await (0, import_promises.writeFile)(picPath, attachedPic.data);
210
- args.push(`-i ${quotePath(picPath)}`);
211
- args.push(`-map 0:a -map 1:v`);
212
- args.push(`-disposition:v:0 attached_pic`);
245
+ args.push(`-i ${picPath}`);
246
+ metadataArgs.push(`-map 2:v`);
247
+ metadataArgs.push(`-disposition:v:0 attached_pic`);
213
248
  if (attachedPic.name) {
214
249
  metadataArgs.push(
215
250
  `-metadata:s:v title="${escapeQuotes(attachedPic.name)}"`
package/dist/ffmpeg.d.cts CHANGED
@@ -15,6 +15,7 @@ type AttachedPic = {
15
15
  description?: string | undefined;
16
16
  };
17
17
  declare function getTrackMetadata(path: string): Promise<{
18
+ id3v2Version: 3 | 4 | null;
18
19
  duration: number;
19
20
  bitRate: number | undefined;
20
21
  tags: {
@@ -38,8 +39,9 @@ declare function getTrackMetadata(path: string): Promise<{
38
39
  }[];
39
40
  attachedPic: AttachedPic | undefined;
40
41
  }>;
41
- declare function writeTrackMetadata(path: string, metadata: TrackInfo["tags"], attachedPic: AttachedPic | undefined): Promise<void>;
42
+ declare function writeTrackMetadata(path: string, trackInfo: TrackInfo): Promise<void>;
42
43
  declare function getTrackMetadataFromBuffer(buffer: Uint8Array): Promise<{
44
+ id3v2Version: 3 | 4 | null;
43
45
  duration: number;
44
46
  bitRate: number | undefined;
45
47
  tags: {
package/dist/ffmpeg.d.ts CHANGED
@@ -15,6 +15,7 @@ type AttachedPic = {
15
15
  description?: string | undefined;
16
16
  };
17
17
  declare function getTrackMetadata(path: string): Promise<{
18
+ id3v2Version: 3 | 4 | null;
18
19
  duration: number;
19
20
  bitRate: number | undefined;
20
21
  tags: {
@@ -38,8 +39,9 @@ declare function getTrackMetadata(path: string): Promise<{
38
39
  }[];
39
40
  attachedPic: AttachedPic | undefined;
40
41
  }>;
41
- declare function writeTrackMetadata(path: string, metadata: TrackInfo["tags"], attachedPic: AttachedPic | undefined): Promise<void>;
42
+ declare function writeTrackMetadata(path: string, trackInfo: TrackInfo): Promise<void>;
42
43
  declare function getTrackMetadataFromBuffer(buffer: Uint8Array): Promise<{
44
+ id3v2Version: 3 | 4 | null;
43
45
  duration: number;
44
46
  bitRate: number | undefined;
45
47
  tags: {
package/dist/ffmpeg.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./chunk-BIEQXUOY.js";
1
2
  import { exec, execFile } from "node:child_process";
2
3
  import { randomUUID } from "node:crypto";
3
4
  import { writeFileSync } from "node:fs";
@@ -67,9 +68,15 @@ async function getTrackMetadata(path) {
67
68
  const stdout = await execCmd(
68
69
  `ffprobe -i ${quotePath(path)} -v quiet -show_format -show_chapters -show_streams -output_format json`
69
70
  );
70
- const { chapters, streams, format, duration, bit_rate } = JSON.parse(
71
- stdout
72
- );
71
+ const { chapters, streams, format } = JSON.parse(stdout);
72
+ let id3v2Version = null;
73
+ if (extname(path).toLowerCase() === ".mp3") {
74
+ const { stderr: id3v2VersionOut } = await execPromise(
75
+ `ffprobe -i ${quotePath(path)} -v debug`
76
+ );
77
+ const id3v2VersionMatch = id3v2VersionOut.match(/id3v2 ver:(3|4)/);
78
+ id3v2Version = (id3v2VersionMatch == null ? void 0 : id3v2VersionMatch[1]) ?? null;
79
+ }
73
80
  const attachedPicStream = streams.find(
74
81
  (stream) => !!stream.disposition.attached_pic
75
82
  );
@@ -96,8 +103,9 @@ async function getTrackMetadata(path) {
96
103
  ])
97
104
  };
98
105
  return {
99
- duration: parseFloat(duration),
100
- bitRate: bit_rate !== void 0 ? parseFloat(bit_rate) : bit_rate,
106
+ id3v2Version,
107
+ duration: parseFloat(format.duration),
108
+ bitRate: format.bit_rate !== void 0 ? parseFloat(format.bit_rate) : format.bit_rate,
101
109
  tags: {
102
110
  title: format.tags.title ?? format.tags.Title,
103
111
  subtitle: format.tags.subtitle ?? format.tags.Subtitle,
@@ -122,9 +130,16 @@ async function getTrackMetadata(path) {
122
130
  attachedPic
123
131
  };
124
132
  }
125
- async function writeTrackMetadata(path, metadata, attachedPic) {
133
+ function escapeFfmetadata(str) {
134
+ return str.replaceAll(/\\/g, "\\\\").replaceAll(/=/g, "\\=").replaceAll(/;/g, "\\;").replaceAll(/#/g, "\\#").replaceAll(/\n/g, "\\n");
135
+ }
136
+ async function writeTrackMetadata(path, trackInfo) {
137
+ const { tags: metadata, id3v2Version, chapters, attachedPic } = trackInfo;
126
138
  const args = [];
127
139
  const metadataArgs = [];
140
+ if (id3v2Version) {
141
+ metadataArgs.push(`-id3v2_version ${id3v2Version}`);
142
+ }
128
143
  if (metadata.title) {
129
144
  metadataArgs.push(`-metadata title="${escapeQuotes(metadata.title)}"`);
130
145
  }
@@ -166,13 +181,34 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
166
181
  `-metadata publisher="${escapeQuotes(metadata.publisher)}"`
167
182
  );
168
183
  }
184
+ metadataArgs.push("-map 0:a -map_metadata 0");
169
185
  const ext = extname(path);
170
186
  const tmpPath = join(
171
187
  tmpdir(),
172
188
  `storyteller-platform-audiobook-${randomUUID()}${ext}`
173
189
  );
190
+ const chapterFilePath = join(
191
+ tmpdir(),
192
+ `storyteller-platform-audiobook-${randomUUID()}.txt`
193
+ );
174
194
  let picPath = null;
175
195
  try {
196
+ let chapterFileContents = ";FFMETADATA1\n\n";
197
+ for (const chapter of chapters) {
198
+ chapterFileContents += `[CHAPTER]
199
+ TIMEBASE=1/1000
200
+ START=${Math.floor(chapter.startTime * 1e3)}
201
+ END=${Math.floor(chapter.endTime * 1e3)}
202
+ `;
203
+ if (chapter.title) {
204
+ chapterFileContents += `TITLE=${escapeFfmetadata(chapter.title)}
205
+ `;
206
+ }
207
+ chapterFileContents += "\n";
208
+ }
209
+ await writeFile(chapterFilePath, chapterFileContents);
210
+ args.push(`-i ${chapterFilePath}`);
211
+ metadataArgs.push(`-map_chapters 1`);
176
212
  if (attachedPic) {
177
213
  const imageExt = attachedPic.mimeType.split("/")[1];
178
214
  picPath = join(
@@ -180,9 +216,9 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
180
216
  `storyteller-platform-audiobook-${randomUUID()}.${imageExt}`
181
217
  );
182
218
  await writeFile(picPath, attachedPic.data);
183
- args.push(`-i ${quotePath(picPath)}`);
184
- args.push(`-map 0:a -map 1:v`);
185
- args.push(`-disposition:v:0 attached_pic`);
219
+ args.push(`-i ${picPath}`);
220
+ metadataArgs.push(`-map 2:v`);
221
+ metadataArgs.push(`-disposition:v:0 attached_pic`);
186
222
  if (attachedPic.name) {
187
223
  metadataArgs.push(
188
224
  `-metadata:s:v title="${escapeQuotes(attachedPic.name)}"`
package/dist/index.cjs CHANGED
@@ -1,8 +1,14 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __knownSymbol = (name, symbol) => (symbol = Symbol[name]) ? symbol : Symbol.for("Symbol." + name);
9
+ var __typeError = (msg) => {
10
+ throw TypeError(msg);
11
+ };
6
12
  var __export = (target, all) => {
7
13
  for (var name in all)
8
14
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -15,7 +21,56 @@ var __copyProps = (to, from, except, desc) => {
15
21
  }
16
22
  return to;
17
23
  };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
18
32
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+ var __using = (stack, value, async) => {
34
+ if (value != null) {
35
+ if (typeof value !== "object" && typeof value !== "function") __typeError("Object expected");
36
+ var dispose, inner;
37
+ if (async) dispose = value[__knownSymbol("asyncDispose")];
38
+ if (dispose === void 0) {
39
+ dispose = value[__knownSymbol("dispose")];
40
+ if (async) inner = dispose;
41
+ }
42
+ if (typeof dispose !== "function") __typeError("Object not disposable");
43
+ if (inner) dispose = function() {
44
+ try {
45
+ inner.call(this);
46
+ } catch (e) {
47
+ return Promise.reject(e);
48
+ }
49
+ };
50
+ stack.push([async, dispose, value]);
51
+ } else if (async) {
52
+ stack.push([async]);
53
+ }
54
+ return value;
55
+ };
56
+ var __callDispose = (stack, error, hasError) => {
57
+ var E = typeof SuppressedError === "function" ? SuppressedError : function(e, s, m, _) {
58
+ return _ = Error(m), _.name = "SuppressedError", _.error = e, _.suppressed = s, _;
59
+ };
60
+ var fail = (e) => error = hasError ? new E(e, error, "An error was suppressed during disposal") : (hasError = true, e);
61
+ var next = (it) => {
62
+ while (it = stack.pop()) {
63
+ try {
64
+ var result = it[1] && it[1].call(it[2]);
65
+ if (it[0]) return Promise.resolve(result).then(next, (e) => (fail(e), next()));
66
+ } catch (e) {
67
+ fail(e);
68
+ }
69
+ }
70
+ if (hasError) throw error;
71
+ };
72
+ return next();
73
+ };
19
74
  var index_exports = {};
20
75
  __export(index_exports, {
21
76
  AAC_FILE_EXTENSIONS: () => AAC_FILE_EXTENSIONS,
@@ -37,12 +92,30 @@ var import_node_crypto = require("node:crypto");
37
92
  var import_node_fs = require("node:fs");
38
93
  var import_promises = require("node:fs/promises");
39
94
  var import_node_os = require("node:os");
40
- var import_promises2 = require("node:stream/promises");
41
- var import_fslib = require("@yarnpkg/fslib");
42
- var import_libzip = require("@yarnpkg/libzip");
95
+ var util = __toESM(require("node:util"), 1);
43
96
  var import_mime_types = require("mime-types");
97
+ var import_yauzl = __toESM(require("yauzl"), 1);
98
+ var import_yazl = require("yazl");
44
99
  var import_path = require("@storyteller-platform/path");
45
100
  var import_entry = require("./entry.cjs");
101
+ function promisify(api) {
102
+ return function(arg, options) {
103
+ return new Promise(function(resolve, reject) {
104
+ api(arg, options, function(err, response) {
105
+ if (err) {
106
+ reject(err);
107
+ return;
108
+ }
109
+ resolve(response);
110
+ });
111
+ });
112
+ };
113
+ }
114
+ const unzipFromPath = promisify(
115
+ (arg, options, callback) => {
116
+ import_yauzl.default.open(arg, options, callback);
117
+ }
118
+ );
46
119
  const COVER_IMAGE_FILE_EXTENSIONS = [
47
120
  ".jpeg",
48
121
  ".jpg",
@@ -75,7 +148,7 @@ class Audiobook {
75
148
  metadata = {};
76
149
  inputs;
77
150
  entries = [];
78
- tmpDir = void 0;
151
+ extractPath = void 0;
79
152
  isZip;
80
153
  constructor(...inputs) {
81
154
  this.inputs = inputs;
@@ -89,32 +162,56 @@ class Audiobook {
89
162
  }
90
163
  async getEntries() {
91
164
  if (this.isZip) {
92
- const [first] = this.inputs;
93
- const tmpDir = (0, import_path.join)(
94
- (0, import_node_os.tmpdir)(),
95
- `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}`
96
- );
97
- this.tmpDir = tmpDir;
98
- await (0, import_promises.mkdir)(this.tmpDir, { recursive: true });
99
- const zipFs = new import_libzip.ZipFS(first);
100
- const entries = zipFs.getAllFiles();
101
- for (const entry of entries) {
102
- await (0, import_promises.mkdir)((0, import_path.join)(this.tmpDir, (0, import_path.dirname)(entry)), { recursive: true });
103
- await (0, import_promises2.pipeline)(
104
- zipFs.createReadStream(entry),
105
- (0, import_node_fs.createWriteStream)((0, import_path.join)(this.tmpDir, entry))
165
+ var _stack = [];
166
+ try {
167
+ const [first] = this.inputs;
168
+ const extractPath = (0, import_path.join)(
169
+ (0, import_node_os.tmpdir)(),
170
+ `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}`
171
+ );
172
+ this.extractPath = extractPath;
173
+ await (0, import_promises.mkdir)(this.extractPath, { recursive: true });
174
+ const zipfile = await unzipFromPath(first, { lazyEntries: true });
175
+ const stack = __using(_stack, new DisposableStack());
176
+ stack.defer(() => {
177
+ zipfile.close();
178
+ });
179
+ const entries = [];
180
+ const { promise, resolve } = Promise.withResolvers();
181
+ zipfile.on("end", () => {
182
+ resolve();
183
+ });
184
+ const openReadStream = util.promisify(
185
+ zipfile.openReadStream.bind(zipfile)
106
186
  );
187
+ zipfile.readEntry();
188
+ zipfile.on("entry", async (entry) => {
189
+ if (entry.fileName.endsWith("/")) {
190
+ zipfile.readEntry();
191
+ } else {
192
+ entries.push(entry.fileName);
193
+ const readStream = await openReadStream(entry);
194
+ readStream.on("end", function() {
195
+ zipfile.readEntry();
196
+ });
197
+ const writePath = (0, import_path.join)(extractPath, entry.fileName);
198
+ await (0, import_promises.mkdir)((0, import_path.dirname)(writePath), { recursive: true });
199
+ readStream.pipe((0, import_node_fs.createWriteStream)(writePath));
200
+ }
201
+ });
202
+ await promise;
203
+ return entries.filter(
204
+ (entry) => AUDIO_FILE_EXTENSIONS.includes(
205
+ (0, import_path.extname)(entry)
206
+ )
207
+ ).map((entry) => new import_entry.AudiobookEntry((0, import_path.join)(extractPath, entry)));
208
+ } catch (_) {
209
+ var _error = _, _hasError = true;
210
+ } finally {
211
+ __callDispose(_stack, _error, _hasError);
107
212
  }
108
- zipFs.discardAndClose();
109
- return entries.filter(
110
- (entry) => AUDIO_FILE_EXTENSIONS.includes(
111
- (0, import_path.extname)(entry)
112
- )
113
- ).map((entry) => new import_entry.AudiobookEntry((0, import_path.join)(tmpDir, entry)));
114
213
  } else {
115
- return this.inputs.map(
116
- (input) => new import_entry.AudiobookEntry(input)
117
- );
214
+ return this.inputs.map((input) => new import_entry.AudiobookEntry(input));
118
215
  }
119
216
  }
120
217
  async getFirstValue(getter) {
@@ -234,6 +331,13 @@ class Audiobook {
234
331
  this.metadata.chapters = chapters;
235
332
  return chapters;
236
333
  }
334
+ async setChapters(chapters) {
335
+ if (this.entries.length > 1) {
336
+ throw new Error("Unable to set chapters for multi-file audiobook");
337
+ }
338
+ this.metadata.chapters = chapters;
339
+ await this.setValue((entry) => entry.setChapters(chapters));
340
+ }
237
341
  async getResources() {
238
342
  if (this.metadata.resources) return this.metadata.resources;
239
343
  const resources = [];
@@ -252,30 +356,58 @@ class Audiobook {
252
356
  for (const entry of this.entries) {
253
357
  await entry.saveAndClose();
254
358
  }
255
- if (this.isZip) {
256
- const [first] = this.inputs;
257
- const zipFs = new import_libzip.ZipFS(first);
258
- const tmpDir = this.tmpDir;
259
- const entries = await (0, import_promises.readdir)(tmpDir, { recursive: true });
260
- for (const entry of entries) {
261
- await zipFs.mkdirPromise(
262
- import_fslib.ppath.join(import_fslib.PortablePath.root, (0, import_path.dirname)(entry)),
263
- { recursive: true }
264
- );
265
- await (0, import_promises2.pipeline)(
266
- (0, import_node_fs.createReadStream)((0, import_path.join)(tmpDir, entry)),
267
- zipFs.createWriteStream(
268
- import_fslib.ppath.join(import_fslib.PortablePath.root, entry)
269
- )
359
+ if (this.isZip && this.extractPath) {
360
+ var _stack = [];
361
+ try {
362
+ const tmpArchivePath = (0, import_path.join)(
363
+ (0, import_node_os.tmpdir)(),
364
+ `storyteller-platform-epub-${(0, import_node_crypto.randomUUID)()}`
270
365
  );
366
+ const [first] = this.inputs;
367
+ const { promise, resolve } = Promise.withResolvers();
368
+ const zipfile = new import_yazl.ZipFile();
369
+ const writeStream = (0, import_node_fs.createWriteStream)(tmpArchivePath);
370
+ writeStream.on("close", () => {
371
+ resolve();
372
+ });
373
+ const stack = __using(_stack, new AsyncDisposableStack(), true);
374
+ stack.defer(async () => {
375
+ writeStream.close();
376
+ await (0, import_promises.rm)(tmpArchivePath, { force: true });
377
+ });
378
+ zipfile.outputStream.pipe(writeStream);
379
+ const entries = await (0, import_promises.readdir)(this.extractPath, {
380
+ recursive: true,
381
+ withFileTypes: true
382
+ });
383
+ for (const entry of entries) {
384
+ if (entry.isDirectory()) continue;
385
+ zipfile.addFile(
386
+ (0, import_path.join)(entry.parentPath, entry.name),
387
+ (0, import_path.join)(entry.parentPath, entry.name).replace(
388
+ `${this.extractPath}/`,
389
+ ""
390
+ )
391
+ );
392
+ }
393
+ zipfile.end();
394
+ await promise;
395
+ await (0, import_promises.cp)(tmpArchivePath, first);
396
+ return;
397
+ } catch (_) {
398
+ var _error = _, _hasError = true;
399
+ } finally {
400
+ var _promise = __callDispose(_stack, _error, _hasError);
401
+ _promise && await _promise;
271
402
  }
272
- zipFs.saveAndClose();
273
- return;
274
403
  }
275
404
  }
276
405
  discardAndClose() {
277
406
  this.inputs = [""];
278
407
  this.entries = [];
408
+ if (this.extractPath) {
409
+ (0, import_node_fs.rmSync)(this.extractPath, { recursive: true });
410
+ }
279
411
  }
280
412
  [Symbol.dispose]() {
281
413
  this.discardAndClose();
package/dist/index.d.cts CHANGED
@@ -30,7 +30,7 @@ declare class Audiobook {
30
30
  protected metadata: AudiobookMetadata;
31
31
  private inputs;
32
32
  private entries;
33
- private tmpDir;
33
+ private extractPath;
34
34
  private isZip;
35
35
  private constructor();
36
36
  static from(...inputs: AudiobookInputs): Promise<Audiobook>;
@@ -55,6 +55,7 @@ declare class Audiobook {
55
55
  setReleaseDate(date: Date): Promise<void>;
56
56
  getDuration(): Promise<number>;
57
57
  getChapters(): Promise<AudiobookChapter[]>;
58
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
58
59
  getResources(): Promise<AudiobookResource[]>;
59
60
  saveAndClose(): Promise<void>;
60
61
  discardAndClose(): void;
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ declare class Audiobook {
30
30
  protected metadata: AudiobookMetadata;
31
31
  private inputs;
32
32
  private entries;
33
- private tmpDir;
33
+ private extractPath;
34
34
  private isZip;
35
35
  private constructor();
36
36
  static from(...inputs: AudiobookInputs): Promise<Audiobook>;
@@ -55,6 +55,7 @@ declare class Audiobook {
55
55
  setReleaseDate(date: Date): Promise<void>;
56
56
  getDuration(): Promise<number>;
57
57
  getChapters(): Promise<AudiobookChapter[]>;
58
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
58
59
  getResources(): Promise<AudiobookResource[]>;
59
60
  saveAndClose(): Promise<void>;
60
61
  discardAndClose(): void;
package/dist/index.js CHANGED
@@ -1,13 +1,35 @@
1
+ import {
2
+ __callDispose,
3
+ __using
4
+ } from "./chunk-BIEQXUOY.js";
1
5
  import { randomUUID } from "node:crypto";
2
- import { createReadStream, createWriteStream } from "node:fs";
3
- import { mkdir, readFile, readdir } from "node:fs/promises";
6
+ import { createWriteStream, rmSync } from "node:fs";
7
+ import { cp, mkdir, readFile, readdir, rm } from "node:fs/promises";
4
8
  import { tmpdir } from "node:os";
5
- import { pipeline } from "node:stream/promises";
6
- import { PortablePath, ppath } from "@yarnpkg/fslib";
7
- import { ZipFS } from "@yarnpkg/libzip";
9
+ import * as util from "node:util";
8
10
  import { lookup } from "mime-types";
11
+ import yauzl from "yauzl";
12
+ import { ZipFile } from "yazl";
9
13
  import { basename, dirname, extname, join } from "@storyteller-platform/path";
10
14
  import { AudiobookEntry } from "./entry.js";
15
+ function promisify(api) {
16
+ return function(arg, options) {
17
+ return new Promise(function(resolve, reject) {
18
+ api(arg, options, function(err, response) {
19
+ if (err) {
20
+ reject(err);
21
+ return;
22
+ }
23
+ resolve(response);
24
+ });
25
+ });
26
+ };
27
+ }
28
+ const unzipFromPath = promisify(
29
+ (arg, options, callback) => {
30
+ yauzl.open(arg, options, callback);
31
+ }
32
+ );
11
33
  const COVER_IMAGE_FILE_EXTENSIONS = [
12
34
  ".jpeg",
13
35
  ".jpg",
@@ -40,7 +62,7 @@ class Audiobook {
40
62
  metadata = {};
41
63
  inputs;
42
64
  entries = [];
43
- tmpDir = void 0;
65
+ extractPath = void 0;
44
66
  isZip;
45
67
  constructor(...inputs) {
46
68
  this.inputs = inputs;
@@ -54,32 +76,56 @@ class Audiobook {
54
76
  }
55
77
  async getEntries() {
56
78
  if (this.isZip) {
57
- const [first] = this.inputs;
58
- const tmpDir = join(
59
- tmpdir(),
60
- `storyteller-platform-audiobook-${randomUUID()}`
61
- );
62
- this.tmpDir = tmpDir;
63
- await mkdir(this.tmpDir, { recursive: true });
64
- const zipFs = new ZipFS(first);
65
- const entries = zipFs.getAllFiles();
66
- for (const entry of entries) {
67
- await mkdir(join(this.tmpDir, dirname(entry)), { recursive: true });
68
- await pipeline(
69
- zipFs.createReadStream(entry),
70
- createWriteStream(join(this.tmpDir, entry))
79
+ var _stack = [];
80
+ try {
81
+ const [first] = this.inputs;
82
+ const extractPath = join(
83
+ tmpdir(),
84
+ `storyteller-platform-audiobook-${randomUUID()}`
71
85
  );
86
+ this.extractPath = extractPath;
87
+ await mkdir(this.extractPath, { recursive: true });
88
+ const zipfile = await unzipFromPath(first, { lazyEntries: true });
89
+ const stack = __using(_stack, new DisposableStack());
90
+ stack.defer(() => {
91
+ zipfile.close();
92
+ });
93
+ const entries = [];
94
+ const { promise, resolve } = Promise.withResolvers();
95
+ zipfile.on("end", () => {
96
+ resolve();
97
+ });
98
+ const openReadStream = util.promisify(
99
+ zipfile.openReadStream.bind(zipfile)
100
+ );
101
+ zipfile.readEntry();
102
+ zipfile.on("entry", async (entry) => {
103
+ if (entry.fileName.endsWith("/")) {
104
+ zipfile.readEntry();
105
+ } else {
106
+ entries.push(entry.fileName);
107
+ const readStream = await openReadStream(entry);
108
+ readStream.on("end", function() {
109
+ zipfile.readEntry();
110
+ });
111
+ const writePath = join(extractPath, entry.fileName);
112
+ await mkdir(dirname(writePath), { recursive: true });
113
+ readStream.pipe(createWriteStream(writePath));
114
+ }
115
+ });
116
+ await promise;
117
+ return entries.filter(
118
+ (entry) => AUDIO_FILE_EXTENSIONS.includes(
119
+ extname(entry)
120
+ )
121
+ ).map((entry) => new AudiobookEntry(join(extractPath, entry)));
122
+ } catch (_) {
123
+ var _error = _, _hasError = true;
124
+ } finally {
125
+ __callDispose(_stack, _error, _hasError);
72
126
  }
73
- zipFs.discardAndClose();
74
- return entries.filter(
75
- (entry) => AUDIO_FILE_EXTENSIONS.includes(
76
- extname(entry)
77
- )
78
- ).map((entry) => new AudiobookEntry(join(tmpDir, entry)));
79
127
  } else {
80
- return this.inputs.map(
81
- (input) => new AudiobookEntry(input)
82
- );
128
+ return this.inputs.map((input) => new AudiobookEntry(input));
83
129
  }
84
130
  }
85
131
  async getFirstValue(getter) {
@@ -199,6 +245,13 @@ class Audiobook {
199
245
  this.metadata.chapters = chapters;
200
246
  return chapters;
201
247
  }
248
+ async setChapters(chapters) {
249
+ if (this.entries.length > 1) {
250
+ throw new Error("Unable to set chapters for multi-file audiobook");
251
+ }
252
+ this.metadata.chapters = chapters;
253
+ await this.setValue((entry) => entry.setChapters(chapters));
254
+ }
202
255
  async getResources() {
203
256
  if (this.metadata.resources) return this.metadata.resources;
204
257
  const resources = [];
@@ -217,30 +270,58 @@ class Audiobook {
217
270
  for (const entry of this.entries) {
218
271
  await entry.saveAndClose();
219
272
  }
220
- if (this.isZip) {
221
- const [first] = this.inputs;
222
- const zipFs = new ZipFS(first);
223
- const tmpDir = this.tmpDir;
224
- const entries = await readdir(tmpDir, { recursive: true });
225
- for (const entry of entries) {
226
- await zipFs.mkdirPromise(
227
- ppath.join(PortablePath.root, dirname(entry)),
228
- { recursive: true }
229
- );
230
- await pipeline(
231
- createReadStream(join(tmpDir, entry)),
232
- zipFs.createWriteStream(
233
- ppath.join(PortablePath.root, entry)
234
- )
273
+ if (this.isZip && this.extractPath) {
274
+ var _stack = [];
275
+ try {
276
+ const tmpArchivePath = join(
277
+ tmpdir(),
278
+ `storyteller-platform-epub-${randomUUID()}`
235
279
  );
280
+ const [first] = this.inputs;
281
+ const { promise, resolve } = Promise.withResolvers();
282
+ const zipfile = new ZipFile();
283
+ const writeStream = createWriteStream(tmpArchivePath);
284
+ writeStream.on("close", () => {
285
+ resolve();
286
+ });
287
+ const stack = __using(_stack, new AsyncDisposableStack(), true);
288
+ stack.defer(async () => {
289
+ writeStream.close();
290
+ await rm(tmpArchivePath, { force: true });
291
+ });
292
+ zipfile.outputStream.pipe(writeStream);
293
+ const entries = await readdir(this.extractPath, {
294
+ recursive: true,
295
+ withFileTypes: true
296
+ });
297
+ for (const entry of entries) {
298
+ if (entry.isDirectory()) continue;
299
+ zipfile.addFile(
300
+ join(entry.parentPath, entry.name),
301
+ join(entry.parentPath, entry.name).replace(
302
+ `${this.extractPath}/`,
303
+ ""
304
+ )
305
+ );
306
+ }
307
+ zipfile.end();
308
+ await promise;
309
+ await cp(tmpArchivePath, first);
310
+ return;
311
+ } catch (_) {
312
+ var _error = _, _hasError = true;
313
+ } finally {
314
+ var _promise = __callDispose(_stack, _error, _hasError);
315
+ _promise && await _promise;
236
316
  }
237
- zipFs.saveAndClose();
238
- return;
239
317
  }
240
318
  }
241
319
  discardAndClose() {
242
320
  this.inputs = [""];
243
321
  this.entries = [];
322
+ if (this.extractPath) {
323
+ rmSync(this.extractPath, { recursive: true });
324
+ }
244
325
  }
245
326
  [Symbol.dispose]() {
246
327
  this.discardAndClose();
package/dist/mime.js CHANGED
@@ -1,3 +1,4 @@
1
+ import "./chunk-BIEQXUOY.js";
1
2
  const COVER_IMAGE_FILE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".svg"];
2
3
  const MP3_FILE_EXTENSIONS = [".mp3"];
3
4
  const MPEG4_FILE_EXTENSIONS = [".mp4", ".m4a", ".m4b"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyteller-platform/audiobook",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",
@@ -32,6 +32,7 @@
32
32
  "devDependencies": {
33
33
  "@storyteller-platform/tsup": "^0.1.0",
34
34
  "@tsconfig/strictest": "^2.0.5",
35
+ "@types/yazl": "^3",
35
36
  "markdown-toc": "^1.2.0",
36
37
  "remark-toc": "^9.0.0",
37
38
  "tsup": "^8.5.0",
@@ -46,10 +47,11 @@
46
47
  "@storyteller-platform/path": "^0.1.1",
47
48
  "@types/mime-types": "^2",
48
49
  "@types/node": "^24.0.0",
49
- "@yarnpkg/fslib": "^3.1.3",
50
- "@yarnpkg/libzip": "^3.2.2",
50
+ "@types/yauzl": "^2.10.3",
51
51
  "mime-types": "^3.0.1",
52
- "music-metadata": "^11.9.0"
52
+ "music-metadata": "^11.9.0",
53
+ "yauzl": "^3.2.0",
54
+ "yazl": "^3.3.1"
53
55
  },
54
56
  "publishConfig": {
55
57
  "access": "public",