@storyteller-platform/audiobook 0.3.4 → 0.3.6

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.
package/dist/entry.cjs CHANGED
@@ -144,9 +144,28 @@ class AudiobookEntry {
144
144
  start: chapter.startTime
145
145
  }));
146
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
+ }
147
166
  async saveAndClose() {
148
167
  const info = await this.getInfo();
149
- await (0, import_ffmpeg.writeTrackMetadata)(this.filename, info.tags, info.attachedPic);
168
+ await (0, import_ffmpeg.writeTrackMetadata)(this.filename, info);
150
169
  }
151
170
  }
152
171
  // Annotate the CommonJS export names for ESM import in node:
package/dist/entry.d.cts CHANGED
@@ -33,6 +33,7 @@ declare class AudiobookEntry {
33
33
  getBitRate(): Promise<number | undefined>;
34
34
  getResource(): Promise<AudiobookResource>;
35
35
  getChapters(): Promise<AudiobookChapter[]>;
36
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
36
37
  saveAndClose(): Promise<void>;
37
38
  }
38
39
 
package/dist/entry.d.ts CHANGED
@@ -33,6 +33,7 @@ declare class AudiobookEntry {
33
33
  getBitRate(): Promise<number | undefined>;
34
34
  getResource(): Promise<AudiobookResource>;
35
35
  getChapters(): Promise<AudiobookChapter[]>;
36
+ setChapters(chapters: AudiobookChapter[]): Promise<void>;
36
37
  saveAndClose(): Promise<void>;
37
38
  }
38
39
 
package/dist/entry.js CHANGED
@@ -125,9 +125,28 @@ class AudiobookEntry {
125
125
  start: chapter.startTime
126
126
  }));
127
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
+ }
128
147
  async saveAndClose() {
129
148
  const info = await this.getInfo();
130
- await writeTrackMetadata(this.filename, info.tags, info.attachedPic);
149
+ await writeTrackMetadata(this.filename, info);
131
150
  }
132
151
  }
133
152
  export {
package/dist/ffmpeg.cjs CHANGED
@@ -90,11 +90,19 @@ function lookup(codecName) {
90
90
  }
91
91
  }
92
92
  async function getTrackMetadata(path) {
93
- var _a, _b;
93
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
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
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
+ }
98
106
  const attachedPicStream = streams.find(
99
107
  (stream) => !!stream.disposition.attached_pic
100
108
  );
@@ -121,20 +129,21 @@ async function getTrackMetadata(path) {
121
129
  ])
122
130
  };
123
131
  return {
132
+ id3v2Version,
124
133
  duration: parseFloat(format.duration),
125
134
  bitRate: format.bit_rate !== void 0 ? parseFloat(format.bit_rate) : format.bit_rate,
126
135
  tags: {
127
- title: format.tags.title ?? format.tags.Title,
128
- subtitle: format.tags.subtitle ?? format.tags.Subtitle,
129
- date: format.tags.date ?? format.tags.Date,
130
- album: format.tags.album ?? format.tags.Album,
131
- albumArtist: format.tags.album_artist ?? format.tags.Album_Artist,
132
- artist: format.tags.artist ?? format.tags.Artist,
133
- performer: format.tags.performer ?? format.tags.Performer,
134
- composer: format.tags.composer ?? format.tags.Composer,
135
- comment: format.tags.comment ?? format.tags.Comment,
136
- description: format.tags.description ?? format.tags.Description,
137
- publisher: format.tags.publisher ?? format.tags.Publisher
136
+ title: ((_c = format.tags) == null ? void 0 : _c.title) ?? ((_d = format.tags) == null ? void 0 : _d.Title),
137
+ subtitle: ((_e = format.tags) == null ? void 0 : _e.subtitle) ?? ((_f = format.tags) == null ? void 0 : _f.Subtitle),
138
+ date: ((_g = format.tags) == null ? void 0 : _g.date) ?? ((_h = format.tags) == null ? void 0 : _h.Date),
139
+ album: ((_i = format.tags) == null ? void 0 : _i.album) ?? ((_j = format.tags) == null ? void 0 : _j.Album),
140
+ albumArtist: ((_k = format.tags) == null ? void 0 : _k.album_artist) ?? ((_l = format.tags) == null ? void 0 : _l.Album_Artist),
141
+ artist: ((_m = format.tags) == null ? void 0 : _m.artist) ?? ((_n = format.tags) == null ? void 0 : _n.Artist),
142
+ performer: ((_o = format.tags) == null ? void 0 : _o.performer) ?? ((_p = format.tags) == null ? void 0 : _p.Performer),
143
+ composer: ((_q = format.tags) == null ? void 0 : _q.composer) ?? ((_r = format.tags) == null ? void 0 : _r.Composer),
144
+ comment: ((_s = format.tags) == null ? void 0 : _s.comment) ?? ((_t = format.tags) == null ? void 0 : _t.Comment),
145
+ description: ((_u = format.tags) == null ? void 0 : _u.description) ?? ((_v = format.tags) == null ? void 0 : _v.Description),
146
+ publisher: ((_w = format.tags) == null ? void 0 : _w.publisher) ?? ((_x = format.tags) == null ? void 0 : _x.Publisher)
138
147
  },
139
148
  chapters: chapters.map(
140
149
  (chapter) => ({
@@ -147,9 +156,16 @@ async function getTrackMetadata(path) {
147
156
  attachedPic
148
157
  };
149
158
  }
150
- 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;
151
164
  const args = [];
152
165
  const metadataArgs = [];
166
+ if (id3v2Version) {
167
+ metadataArgs.push(`-id3v2_version ${id3v2Version}`);
168
+ }
153
169
  if (metadata.title) {
154
170
  metadataArgs.push(`-metadata title="${escapeQuotes(metadata.title)}"`);
155
171
  }
@@ -191,13 +207,34 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
191
207
  `-metadata publisher="${escapeQuotes(metadata.publisher)}"`
192
208
  );
193
209
  }
210
+ metadataArgs.push("-map 0:a -map_metadata 0");
194
211
  const ext = (0, import_path.extname)(path);
195
212
  const tmpPath = (0, import_node_path.join)(
196
213
  (0, import_node_os.tmpdir)(),
197
214
  `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}${ext}`
198
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
+ );
199
220
  let picPath = null;
200
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`);
201
238
  if (attachedPic) {
202
239
  const imageExt = attachedPic.mimeType.split("/")[1];
203
240
  picPath = (0, import_node_path.join)(
@@ -205,9 +242,9 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
205
242
  `storyteller-platform-audiobook-${(0, import_node_crypto.randomUUID)()}.${imageExt}`
206
243
  );
207
244
  await (0, import_promises.writeFile)(picPath, attachedPic.data);
208
- args.push(`-i ${quotePath(picPath)}`);
209
- args.push(`-map 0:a -map 1:v`);
210
- 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`);
211
248
  if (attachedPic.name) {
212
249
  metadataArgs.push(
213
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
@@ -64,11 +64,19 @@ function lookup(codecName) {
64
64
  }
65
65
  }
66
66
  async function getTrackMetadata(path) {
67
- var _a, _b;
67
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
68
68
  const stdout = await execCmd(
69
69
  `ffprobe -i ${quotePath(path)} -v quiet -show_format -show_chapters -show_streams -output_format json`
70
70
  );
71
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
+ }
72
80
  const attachedPicStream = streams.find(
73
81
  (stream) => !!stream.disposition.attached_pic
74
82
  );
@@ -95,20 +103,21 @@ async function getTrackMetadata(path) {
95
103
  ])
96
104
  };
97
105
  return {
106
+ id3v2Version,
98
107
  duration: parseFloat(format.duration),
99
108
  bitRate: format.bit_rate !== void 0 ? parseFloat(format.bit_rate) : format.bit_rate,
100
109
  tags: {
101
- title: format.tags.title ?? format.tags.Title,
102
- subtitle: format.tags.subtitle ?? format.tags.Subtitle,
103
- date: format.tags.date ?? format.tags.Date,
104
- album: format.tags.album ?? format.tags.Album,
105
- albumArtist: format.tags.album_artist ?? format.tags.Album_Artist,
106
- artist: format.tags.artist ?? format.tags.Artist,
107
- performer: format.tags.performer ?? format.tags.Performer,
108
- composer: format.tags.composer ?? format.tags.Composer,
109
- comment: format.tags.comment ?? format.tags.Comment,
110
- description: format.tags.description ?? format.tags.Description,
111
- publisher: format.tags.publisher ?? format.tags.Publisher
110
+ title: ((_c = format.tags) == null ? void 0 : _c.title) ?? ((_d = format.tags) == null ? void 0 : _d.Title),
111
+ subtitle: ((_e = format.tags) == null ? void 0 : _e.subtitle) ?? ((_f = format.tags) == null ? void 0 : _f.Subtitle),
112
+ date: ((_g = format.tags) == null ? void 0 : _g.date) ?? ((_h = format.tags) == null ? void 0 : _h.Date),
113
+ album: ((_i = format.tags) == null ? void 0 : _i.album) ?? ((_j = format.tags) == null ? void 0 : _j.Album),
114
+ albumArtist: ((_k = format.tags) == null ? void 0 : _k.album_artist) ?? ((_l = format.tags) == null ? void 0 : _l.Album_Artist),
115
+ artist: ((_m = format.tags) == null ? void 0 : _m.artist) ?? ((_n = format.tags) == null ? void 0 : _n.Artist),
116
+ performer: ((_o = format.tags) == null ? void 0 : _o.performer) ?? ((_p = format.tags) == null ? void 0 : _p.Performer),
117
+ composer: ((_q = format.tags) == null ? void 0 : _q.composer) ?? ((_r = format.tags) == null ? void 0 : _r.Composer),
118
+ comment: ((_s = format.tags) == null ? void 0 : _s.comment) ?? ((_t = format.tags) == null ? void 0 : _t.Comment),
119
+ description: ((_u = format.tags) == null ? void 0 : _u.description) ?? ((_v = format.tags) == null ? void 0 : _v.Description),
120
+ publisher: ((_w = format.tags) == null ? void 0 : _w.publisher) ?? ((_x = format.tags) == null ? void 0 : _x.Publisher)
112
121
  },
113
122
  chapters: chapters.map(
114
123
  (chapter) => ({
@@ -121,9 +130,16 @@ async function getTrackMetadata(path) {
121
130
  attachedPic
122
131
  };
123
132
  }
124
- 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;
125
138
  const args = [];
126
139
  const metadataArgs = [];
140
+ if (id3v2Version) {
141
+ metadataArgs.push(`-id3v2_version ${id3v2Version}`);
142
+ }
127
143
  if (metadata.title) {
128
144
  metadataArgs.push(`-metadata title="${escapeQuotes(metadata.title)}"`);
129
145
  }
@@ -165,13 +181,34 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
165
181
  `-metadata publisher="${escapeQuotes(metadata.publisher)}"`
166
182
  );
167
183
  }
184
+ metadataArgs.push("-map 0:a -map_metadata 0");
168
185
  const ext = extname(path);
169
186
  const tmpPath = join(
170
187
  tmpdir(),
171
188
  `storyteller-platform-audiobook-${randomUUID()}${ext}`
172
189
  );
190
+ const chapterFilePath = join(
191
+ tmpdir(),
192
+ `storyteller-platform-audiobook-${randomUUID()}.txt`
193
+ );
173
194
  let picPath = null;
174
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`);
175
212
  if (attachedPic) {
176
213
  const imageExt = attachedPic.mimeType.split("/")[1];
177
214
  picPath = join(
@@ -179,9 +216,9 @@ async function writeTrackMetadata(path, metadata, attachedPic) {
179
216
  `storyteller-platform-audiobook-${randomUUID()}.${imageExt}`
180
217
  );
181
218
  await writeFile(picPath, attachedPic.data);
182
- args.push(`-i ${quotePath(picPath)}`);
183
- args.push(`-map 0:a -map 1:v`);
184
- 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`);
185
222
  if (attachedPic.name) {
186
223
  metadataArgs.push(
187
224
  `-metadata:s:v title="${escapeQuotes(attachedPic.name)}"`
package/dist/index.cjs CHANGED
@@ -331,6 +331,13 @@ class Audiobook {
331
331
  this.metadata.chapters = chapters;
332
332
  return chapters;
333
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
+ }
334
341
  async getResources() {
335
342
  if (this.metadata.resources) return this.metadata.resources;
336
343
  const resources = [];
package/dist/index.d.cts CHANGED
@@ -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
@@ -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
@@ -245,6 +245,13 @@ class Audiobook {
245
245
  this.metadata.chapters = chapters;
246
246
  return chapters;
247
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
+ }
248
255
  async getResources() {
249
256
  if (this.metadata.resources) return this.metadata.resources;
250
257
  const resources = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storyteller-platform/audiobook",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "module": "dist/index.js",