@mtillmann/chapters 0.0.1
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/index.d.ts +295 -0
- package/dist/index.js +1339 -0
- package/package.json +62 -0
- package/readme.md +151 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/Formats/AppleChapters.ts
|
|
8
|
+
import { JSDOM as JSDOM2 } from "jsdom";
|
|
9
|
+
|
|
10
|
+
// src/util.ts
|
|
11
|
+
var util_exports = {};
|
|
12
|
+
__export(util_exports, {
|
|
13
|
+
Float: () => Float,
|
|
14
|
+
Floats: () => Floats,
|
|
15
|
+
Int: () => Int,
|
|
16
|
+
Ints: () => Ints,
|
|
17
|
+
NPTToSeconds: () => NPTToSeconds,
|
|
18
|
+
enforceMilliseconds: () => enforceMilliseconds,
|
|
19
|
+
escapeRegExpCharacters: () => escapeRegExpCharacters,
|
|
20
|
+
formatBytes: () => formatBytes,
|
|
21
|
+
hash: () => hash,
|
|
22
|
+
indenter: () => indenter,
|
|
23
|
+
secondsToNPT: () => secondsToNPT,
|
|
24
|
+
secondsToTimestamp: () => secondsToTimestamp,
|
|
25
|
+
stringToLines: () => stringToLines,
|
|
26
|
+
timestampToSeconds: () => timestampToSeconds,
|
|
27
|
+
toSeconds: () => toSeconds,
|
|
28
|
+
zeroPad: () => zeroPad
|
|
29
|
+
});
|
|
30
|
+
function zeroPad(num, len = 3) {
|
|
31
|
+
return String(num).padStart(len, "0");
|
|
32
|
+
}
|
|
33
|
+
function secondsToTimestamp(seconds, options = {}) {
|
|
34
|
+
options = {
|
|
35
|
+
hours: true,
|
|
36
|
+
milliseconds: false,
|
|
37
|
+
...options
|
|
38
|
+
};
|
|
39
|
+
const date = new Date(Int(seconds) * 1e3).toISOString();
|
|
40
|
+
if (date.slice(11, 13) !== "00") {
|
|
41
|
+
options.hours = true;
|
|
42
|
+
}
|
|
43
|
+
const hms = date.slice(options.hours ? 11 : 14, 19);
|
|
44
|
+
if (options.milliseconds) {
|
|
45
|
+
let fraction = "000";
|
|
46
|
+
if (seconds.toString().includes(".")) {
|
|
47
|
+
fraction = (String(seconds).split(".").pop() + "000").slice(0, 3);
|
|
48
|
+
}
|
|
49
|
+
return hms + "." + fraction;
|
|
50
|
+
}
|
|
51
|
+
return hms;
|
|
52
|
+
}
|
|
53
|
+
function NPTToSeconds(npt) {
|
|
54
|
+
const parts = npt.split(".");
|
|
55
|
+
const hms = parts[0].split(":");
|
|
56
|
+
const ms = parts.length > 1 ? Int(parts[1]) : 0;
|
|
57
|
+
while (hms.length < 3) {
|
|
58
|
+
hms.unshift("0");
|
|
59
|
+
}
|
|
60
|
+
const [hours, minutes, seconds] = Ints(hms);
|
|
61
|
+
return timestampToSeconds(`${zeroPad(hours.toString(), 2)}:${zeroPad(minutes.toString(), 2)}:${zeroPad(seconds.toString(), 2)}.${zeroPad(ms.toString(), 3)}`);
|
|
62
|
+
}
|
|
63
|
+
function secondsToNPT(seconds) {
|
|
64
|
+
if (seconds === 0) {
|
|
65
|
+
return "0";
|
|
66
|
+
}
|
|
67
|
+
const regularTimestamp = secondsToTimestamp(seconds, { milliseconds: true });
|
|
68
|
+
let [hoursAndMinutesAndSeconds, milliseconds] = regularTimestamp.split(".");
|
|
69
|
+
const [hours, minutes, secondsOnly] = Ints(hoursAndMinutesAndSeconds.split(":"));
|
|
70
|
+
if (milliseconds === "000") {
|
|
71
|
+
milliseconds = "";
|
|
72
|
+
} else {
|
|
73
|
+
milliseconds = "." + milliseconds;
|
|
74
|
+
}
|
|
75
|
+
if (hours === 0 && minutes === 0) {
|
|
76
|
+
return `${secondsOnly}${milliseconds}`;
|
|
77
|
+
}
|
|
78
|
+
const secondsString = zeroPad(secondsOnly, 2);
|
|
79
|
+
if (hours === 0) {
|
|
80
|
+
return `${minutes}:${secondsString}${milliseconds}`;
|
|
81
|
+
}
|
|
82
|
+
const minutesString = zeroPad(minutes, 2);
|
|
83
|
+
return `${hours}:${minutesString}:${secondsString}${milliseconds}`;
|
|
84
|
+
}
|
|
85
|
+
function timestampToSeconds(timestamp, fixedString = false) {
|
|
86
|
+
let [seconds, minutes, hours] = Ints(timestamp.split(":")).reverse();
|
|
87
|
+
let milliseconds = timestamp.split(".").length > 1 ? Int(timestamp.split(".").pop()) : 0;
|
|
88
|
+
if (!hours) {
|
|
89
|
+
hours = 0;
|
|
90
|
+
}
|
|
91
|
+
if (!minutes) {
|
|
92
|
+
minutes = 0;
|
|
93
|
+
}
|
|
94
|
+
if (!seconds) {
|
|
95
|
+
seconds = 0;
|
|
96
|
+
}
|
|
97
|
+
if (milliseconds > 0) {
|
|
98
|
+
milliseconds = milliseconds / 1e3;
|
|
99
|
+
}
|
|
100
|
+
if (seconds > 59) {
|
|
101
|
+
const extraMinutes = Math.floor(seconds / 60);
|
|
102
|
+
minutes += extraMinutes;
|
|
103
|
+
seconds -= extraMinutes * 60;
|
|
104
|
+
}
|
|
105
|
+
if (minutes > 59) {
|
|
106
|
+
const extraHours = Math.floor(minutes / 60);
|
|
107
|
+
hours += extraHours;
|
|
108
|
+
minutes -= extraHours * 60;
|
|
109
|
+
}
|
|
110
|
+
if (fixedString) {
|
|
111
|
+
return Float((hours * 3600 + minutes * 60 + seconds + milliseconds).toFixed(3));
|
|
112
|
+
}
|
|
113
|
+
return hours * 3600 + minutes * 60 + seconds + milliseconds;
|
|
114
|
+
}
|
|
115
|
+
function hash() {
|
|
116
|
+
return (Math.random() + 1).toString(16).substring(7);
|
|
117
|
+
}
|
|
118
|
+
function escapeRegExpCharacters(text) {
|
|
119
|
+
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
|
|
120
|
+
}
|
|
121
|
+
function enforceMilliseconds(seconds) {
|
|
122
|
+
return Float(seconds.toFixed(3));
|
|
123
|
+
}
|
|
124
|
+
function formatBytes(bytes, decimals = 2, format = "kB") {
|
|
125
|
+
if (bytes < 1) {
|
|
126
|
+
return "0 B";
|
|
127
|
+
}
|
|
128
|
+
const k = format === "kB" ? 1e3 : 1024;
|
|
129
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
130
|
+
const sizes = ["", "K", "M", "G", "T", "P", "E", "Z", "Y"];
|
|
131
|
+
const suffix = [format === "kB" ? sizes[i].toLowerCase() : sizes[i], "B"];
|
|
132
|
+
if (format === "KiB") {
|
|
133
|
+
suffix.splice(1, 0, "i");
|
|
134
|
+
}
|
|
135
|
+
return Float(bytes / Math.pow(k, i)).toFixed(decimals) + " " + suffix.join("");
|
|
136
|
+
}
|
|
137
|
+
function Int(value, defaultValue = 0) {
|
|
138
|
+
const i = parseInt(String(value || defaultValue));
|
|
139
|
+
return Number.isNaN(i) ? defaultValue : i;
|
|
140
|
+
}
|
|
141
|
+
function Float(value, defaultValue = 0) {
|
|
142
|
+
const f = parseFloat(String(value || defaultValue));
|
|
143
|
+
return Number.isNaN(f) ? defaultValue : f;
|
|
144
|
+
}
|
|
145
|
+
function Ints(value, defaultValue = 0) {
|
|
146
|
+
return value.map((i) => Int(i, defaultValue));
|
|
147
|
+
}
|
|
148
|
+
function Floats(value, defaultValue = 0) {
|
|
149
|
+
return value.map((i) => Float(i, defaultValue));
|
|
150
|
+
}
|
|
151
|
+
function indenter(spacesPerDepth = 2) {
|
|
152
|
+
const character = spacesPerDepth === 0 ? "" : " ";
|
|
153
|
+
return function(depth, string) {
|
|
154
|
+
return character.repeat(depth * spacesPerDepth) + string;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function stringToLines(string) {
|
|
158
|
+
return string.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => line.trim());
|
|
159
|
+
}
|
|
160
|
+
function toSeconds(input) {
|
|
161
|
+
if (typeof input === "number") {
|
|
162
|
+
return input;
|
|
163
|
+
}
|
|
164
|
+
if (input.includes(":")) {
|
|
165
|
+
return timestampToSeconds(input);
|
|
166
|
+
}
|
|
167
|
+
return Float(input);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// src/Formats/Base.ts
|
|
171
|
+
import filenamify from "filenamify";
|
|
172
|
+
var Base = class {
|
|
173
|
+
supportsPrettyPrint = false;
|
|
174
|
+
chapterTitleTemplate = "Chapter $chapter of $total";
|
|
175
|
+
chapters = [];
|
|
176
|
+
defaultMeta = {
|
|
177
|
+
author: "",
|
|
178
|
+
title: "",
|
|
179
|
+
podcastName: "",
|
|
180
|
+
description: "",
|
|
181
|
+
fileName: "",
|
|
182
|
+
waypoints: false,
|
|
183
|
+
version: "1.2.0"
|
|
184
|
+
};
|
|
185
|
+
// why?!
|
|
186
|
+
meta = { ...this.defaultMeta };
|
|
187
|
+
// meta:MediaItemMeta = {...this.defaultMeta};
|
|
188
|
+
filename = "chapters.json";
|
|
189
|
+
mimeType = "application/json";
|
|
190
|
+
duration = 0;
|
|
191
|
+
isChapterFormat = true;
|
|
192
|
+
static create(input) {
|
|
193
|
+
return new this().from(input);
|
|
194
|
+
}
|
|
195
|
+
from(input) {
|
|
196
|
+
if (!input) {
|
|
197
|
+
throw new Error("No input provided");
|
|
198
|
+
} else if (typeof input === "string") {
|
|
199
|
+
this.parse(input);
|
|
200
|
+
} else if ("isChapterFormat" in input) {
|
|
201
|
+
this.chapters = JSON.parse(JSON.stringify(input.chapters));
|
|
202
|
+
this.meta = { ...this.meta, ...JSON.parse(JSON.stringify(input.meta)) };
|
|
203
|
+
}
|
|
204
|
+
if (this.chapters.length > 0) {
|
|
205
|
+
const chapter = this.chapters.at(-1);
|
|
206
|
+
if (chapter.endTime) {
|
|
207
|
+
this.duration = chapter.endTime;
|
|
208
|
+
} else if (chapter.startTime) {
|
|
209
|
+
this.duration = chapter.startTime;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (this.duration === 0) {
|
|
213
|
+
this.duration = 3600;
|
|
214
|
+
}
|
|
215
|
+
this.bump();
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
detect(inputString) {
|
|
219
|
+
try {
|
|
220
|
+
const data = JSON.parse(inputString);
|
|
221
|
+
const { errors } = this.test(data);
|
|
222
|
+
if (errors.length > 0) {
|
|
223
|
+
throw new Error("data test failed");
|
|
224
|
+
}
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
test(data) {
|
|
231
|
+
if (!("chapters" in data)) {
|
|
232
|
+
return { errors: ['JSON Structure: missing "chapters"'] };
|
|
233
|
+
}
|
|
234
|
+
if (!("version" in data)) {
|
|
235
|
+
return { errors: ['JSON Structure: missing "version"'] };
|
|
236
|
+
}
|
|
237
|
+
return { errors: [] };
|
|
238
|
+
}
|
|
239
|
+
bump(keepDuration = false) {
|
|
240
|
+
this.chapters.sort((a, b) => a.startTime - b.startTime);
|
|
241
|
+
const lastChapter = this.chapters.at(-1);
|
|
242
|
+
if (lastChapter && !keepDuration) {
|
|
243
|
+
this.duration = Math.max(parseFloat(String(this.duration || 0)), parseFloat(String(lastChapter.endTime ?? 0)), parseFloat(String(lastChapter.startTime ?? 0)));
|
|
244
|
+
}
|
|
245
|
+
this.chapters = this.chapters.map((chapter, index) => {
|
|
246
|
+
const endTime = this.endTime(index);
|
|
247
|
+
const duration = endTime - this.chapters[index].startTime;
|
|
248
|
+
const timestampOptions = { hours: false };
|
|
249
|
+
return {
|
|
250
|
+
...{
|
|
251
|
+
id: hash(),
|
|
252
|
+
startTime: 0
|
|
253
|
+
},
|
|
254
|
+
...chapter,
|
|
255
|
+
...{
|
|
256
|
+
endTime,
|
|
257
|
+
duration,
|
|
258
|
+
startTime_hr: secondsToTimestamp(chapter.startTime, timestampOptions),
|
|
259
|
+
endTime_hr: secondsToTimestamp(endTime, timestampOptions),
|
|
260
|
+
duration_hr: secondsToTimestamp(duration, timestampOptions)
|
|
261
|
+
},
|
|
262
|
+
..."toc" in chapter ? {} : { toc: true }
|
|
263
|
+
};
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
endTime(index) {
|
|
267
|
+
return this.chapters[index + 1] ? this.chapters[index + 1].startTime - 1e-3 : this.duration;
|
|
268
|
+
}
|
|
269
|
+
expandFirstToStart() {
|
|
270
|
+
this.chapters[0].startTime = 0;
|
|
271
|
+
this.bump();
|
|
272
|
+
}
|
|
273
|
+
add(chapter) {
|
|
274
|
+
this.chapters.push(chapter);
|
|
275
|
+
this.bump();
|
|
276
|
+
}
|
|
277
|
+
remove(index) {
|
|
278
|
+
if (this.chapters[index]?.img?.slice(0, 5) === "blob:") {
|
|
279
|
+
URL.revokeObjectURL(String(this.chapters[index].img));
|
|
280
|
+
}
|
|
281
|
+
this.chapters.splice(index, 1);
|
|
282
|
+
this.bump();
|
|
283
|
+
}
|
|
284
|
+
to(className) {
|
|
285
|
+
return className.create(this);
|
|
286
|
+
}
|
|
287
|
+
parse(string) {
|
|
288
|
+
const data = JSON.parse(string);
|
|
289
|
+
const { errors } = this.test(data);
|
|
290
|
+
if (errors.length > 0) {
|
|
291
|
+
throw new Error(errors.join(""));
|
|
292
|
+
}
|
|
293
|
+
this.chapters = data.chapters;
|
|
294
|
+
this.chapters = this.chapters.map((chapter) => {
|
|
295
|
+
if ("img" in chapter) {
|
|
296
|
+
if (chapter.img?.slice(0, 4) === "http") {
|
|
297
|
+
chapter.img_type = "absolute";
|
|
298
|
+
} else {
|
|
299
|
+
chapter.img_type = "relative";
|
|
300
|
+
chapter.img_filename = chapter.img;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return chapter;
|
|
304
|
+
});
|
|
305
|
+
this.meta = Object.fromEntries(Object.entries(this.meta).map(([key, value]) => [key, data[key] || value]));
|
|
306
|
+
}
|
|
307
|
+
toString(pretty = false, exportOptions = {}) {
|
|
308
|
+
const options = {
|
|
309
|
+
...{
|
|
310
|
+
imagePrefix: "",
|
|
311
|
+
writeRedundantToc: false,
|
|
312
|
+
writeEndTimes: false
|
|
313
|
+
},
|
|
314
|
+
...exportOptions
|
|
315
|
+
};
|
|
316
|
+
const defaultMetaProperties = Object.keys(this.defaultMeta);
|
|
317
|
+
return JSON.stringify(
|
|
318
|
+
{
|
|
319
|
+
...Object.fromEntries(
|
|
320
|
+
Object.entries(this.meta).filter(([key, value]) => {
|
|
321
|
+
return defaultMetaProperties.includes(key) && value !== "" && value !== false;
|
|
322
|
+
})
|
|
323
|
+
),
|
|
324
|
+
...{
|
|
325
|
+
chapters: this.chapters.map((chapter) => {
|
|
326
|
+
const filtered = {
|
|
327
|
+
startTime: enforceMilliseconds(chapter.startTime)
|
|
328
|
+
};
|
|
329
|
+
if (options.writeEndTimes) {
|
|
330
|
+
filtered.endTime = enforceMilliseconds(chapter.endTime);
|
|
331
|
+
}
|
|
332
|
+
if ("toc" in chapter && chapter.toc === false) {
|
|
333
|
+
filtered.toc = false;
|
|
334
|
+
}
|
|
335
|
+
if (!("toc" in filtered) && options.writeRedundantToc) {
|
|
336
|
+
filtered.toc = true;
|
|
337
|
+
}
|
|
338
|
+
["location", "img", "url", "title"].forEach((property) => {
|
|
339
|
+
const key = property;
|
|
340
|
+
if (key in chapter && String(chapter[key]).trim().length > 0) {
|
|
341
|
+
filtered[key] = chapter[key];
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
if ("img_filename" in chapter && "img" in filtered && chapter.img_type === "relative") {
|
|
345
|
+
filtered.img = filenamify(chapter.img_filename);
|
|
346
|
+
}
|
|
347
|
+
if (options.imagePrefix.trim().length > 0 && "img" in filtered && ["relative", "blob"].includes(chapter.img_type)) {
|
|
348
|
+
filtered.img = options.imagePrefix + String(filtered.img);
|
|
349
|
+
}
|
|
350
|
+
return filtered;
|
|
351
|
+
})
|
|
352
|
+
}
|
|
353
|
+
},
|
|
354
|
+
null,
|
|
355
|
+
pretty ? 2 : 0
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
applyChapterMinLength(seconds) {
|
|
359
|
+
const originalIdMap = this.chapters.map((chapter) => String(chapter.id));
|
|
360
|
+
const newChapters = [];
|
|
361
|
+
let elapsed = 0;
|
|
362
|
+
let currentChapter;
|
|
363
|
+
this.chapters.forEach((chapter) => {
|
|
364
|
+
elapsed += chapter.duration;
|
|
365
|
+
if (!currentChapter) {
|
|
366
|
+
currentChapter = chapter;
|
|
367
|
+
}
|
|
368
|
+
if (elapsed >= seconds) {
|
|
369
|
+
delete currentChapter.endTime;
|
|
370
|
+
delete currentChapter.duration;
|
|
371
|
+
newChapters.push(currentChapter);
|
|
372
|
+
currentChapter = null;
|
|
373
|
+
elapsed = 0;
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
this.chapters = newChapters;
|
|
377
|
+
this.bump();
|
|
378
|
+
const newIdMap = Object.fromEntries(this.chapters.map((c, i) => [c.id, i]));
|
|
379
|
+
return Object.fromEntries(originalIdMap.map((id, index) => {
|
|
380
|
+
return [index, id in newIdMap ? newIdMap[id] : "deleted"];
|
|
381
|
+
}));
|
|
382
|
+
}
|
|
383
|
+
addChapterAt(index, chapter = {}) {
|
|
384
|
+
let startTime = 0;
|
|
385
|
+
if (index > this.chapters.length) {
|
|
386
|
+
const start = this.chapters.at(-1) ? this.chapters.at(-1).startTime : 0;
|
|
387
|
+
startTime = start + (this.duration - start) * 0.5;
|
|
388
|
+
} else if (index === 0) {
|
|
389
|
+
startTime = 0;
|
|
390
|
+
} else {
|
|
391
|
+
const start = this.chapters.at(index - 1).startTime;
|
|
392
|
+
const end = this.chapters.at(index) ? this.chapters.at(index).startTime : this.duration;
|
|
393
|
+
startTime = start + (end - start) * 0.5;
|
|
394
|
+
}
|
|
395
|
+
if (chapter && "startTime" in chapter) {
|
|
396
|
+
delete chapter.startTime;
|
|
397
|
+
}
|
|
398
|
+
this.chapters.push({
|
|
399
|
+
...chapter,
|
|
400
|
+
id: hash(),
|
|
401
|
+
startTime
|
|
402
|
+
});
|
|
403
|
+
this.bump();
|
|
404
|
+
return startTime;
|
|
405
|
+
}
|
|
406
|
+
addChapterAtTime(time, chapter = {}) {
|
|
407
|
+
const startTime = toSeconds(time);
|
|
408
|
+
if (this.chapterExistsAtStartTime(startTime)) {
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
this.chapters.push({
|
|
412
|
+
...chapter,
|
|
413
|
+
id: hash(),
|
|
414
|
+
startTime
|
|
415
|
+
});
|
|
416
|
+
this.bump();
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
rebuildChapterTitles(template) {
|
|
420
|
+
this.chapters.forEach((chapter, index) => {
|
|
421
|
+
this.chapters[index].title = this.getChapterTitle(index, template);
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
ensureTitle(index) {
|
|
425
|
+
return this.chapters[index].title ?? this.getChapterTitle(index);
|
|
426
|
+
}
|
|
427
|
+
getChapterTitle(index, template) {
|
|
428
|
+
template = template ?? this.chapterTitleTemplate;
|
|
429
|
+
return template.replace("$chapter", String(index + 1)).replace("$total", String(this.chapters.length));
|
|
430
|
+
}
|
|
431
|
+
chapterExistsAtStartTime(time) {
|
|
432
|
+
time = toSeconds(time);
|
|
433
|
+
return this.chapters.filter((c) => c.startTime === time).length > 0;
|
|
434
|
+
}
|
|
435
|
+
updateChapterStartTime(index, startTime) {
|
|
436
|
+
const newStartTime = toSeconds(startTime);
|
|
437
|
+
if (this.chapterExistsAtStartTime(newStartTime)) {
|
|
438
|
+
return "timeInUse";
|
|
439
|
+
}
|
|
440
|
+
if (newStartTime > this.duration) {
|
|
441
|
+
this.duration = newStartTime;
|
|
442
|
+
}
|
|
443
|
+
this.chapters[index].startTime = newStartTime;
|
|
444
|
+
this.bump();
|
|
445
|
+
return newStartTime;
|
|
446
|
+
}
|
|
447
|
+
chapterIndexFromStartTime(startTime) {
|
|
448
|
+
startTime = toSeconds(startTime);
|
|
449
|
+
return this.chapters.reduce((newIndex, chapter, index) => {
|
|
450
|
+
if (chapter.startTime === startTime) {
|
|
451
|
+
newIndex = index;
|
|
452
|
+
}
|
|
453
|
+
return newIndex;
|
|
454
|
+
}, 0);
|
|
455
|
+
}
|
|
456
|
+
chapterIndexFromTime(time) {
|
|
457
|
+
const timeStamp = toSeconds(time);
|
|
458
|
+
return this.chapters.reduce((newIndex, chapter, index) => {
|
|
459
|
+
if (timeStamp > chapter.startTime) {
|
|
460
|
+
newIndex = index;
|
|
461
|
+
}
|
|
462
|
+
return newIndex;
|
|
463
|
+
}, null);
|
|
464
|
+
}
|
|
465
|
+
ensureUniqueFilenames() {
|
|
466
|
+
const usedFilenames = [];
|
|
467
|
+
this.chapters = this.chapters.map((chapter) => {
|
|
468
|
+
if (chapter.img_type !== "blob") {
|
|
469
|
+
return chapter;
|
|
470
|
+
}
|
|
471
|
+
chapter.img_filename = filenamify(chapter.img_filename);
|
|
472
|
+
let filename = chapter.img_filename;
|
|
473
|
+
if (usedFilenames.includes(filename)) {
|
|
474
|
+
filename = filename.replace(/(\.\w+)$/, `_${hash()}$1`);
|
|
475
|
+
chapter.img_filename = filename;
|
|
476
|
+
}
|
|
477
|
+
usedFilenames.push(filename);
|
|
478
|
+
return chapter;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
applyImgUri(imgUri) {
|
|
482
|
+
this.chapters.forEach((chapter, i) => {
|
|
483
|
+
if ("img" in chapter) {
|
|
484
|
+
this.chapters[i].img = imgUri.replace(/\/*$/, "") + "/" + chapter.img.replace(/^\/*/, "");
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
// src/Formats/MatroskaXML.ts
|
|
491
|
+
import { JSDOM } from "jsdom";
|
|
492
|
+
var MatroskaXML = class extends Base {
|
|
493
|
+
supportsPrettyPrint = true;
|
|
494
|
+
filename = "matroska-chapters.xml";
|
|
495
|
+
mimeType = "text/xml";
|
|
496
|
+
chapterStringNodeName = "ChapString";
|
|
497
|
+
inputTimeToSeconds(string) {
|
|
498
|
+
return Float(string) / 1e9;
|
|
499
|
+
}
|
|
500
|
+
secondsToOutputTime(seconds) {
|
|
501
|
+
return String(Int(String(seconds * 1e9)));
|
|
502
|
+
}
|
|
503
|
+
detect(inputString) {
|
|
504
|
+
return /^<\?xml/.test(inputString.trim()) && inputString.includes("<Chapters>") && inputString.includes(`<${this.chapterStringNodeName}>`);
|
|
505
|
+
}
|
|
506
|
+
parse(string) {
|
|
507
|
+
if (!this.detect(string)) {
|
|
508
|
+
throw new Error("Input needs xml declaration and a <Chapters> node");
|
|
509
|
+
}
|
|
510
|
+
let dom;
|
|
511
|
+
if (typeof DOMParser !== "undefined") {
|
|
512
|
+
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
513
|
+
} else {
|
|
514
|
+
dom = new JSDOM(string, { contentType: "application/xml" });
|
|
515
|
+
dom = dom.window.document;
|
|
516
|
+
}
|
|
517
|
+
this.chapters = [...dom.querySelectorAll("ChapterAtom")].map((chapter) => {
|
|
518
|
+
return {
|
|
519
|
+
title: String(chapter.querySelector(this.chapterStringNodeName)?.textContent),
|
|
520
|
+
startTime: this.inputTimeToSeconds(String(chapter.querySelector("ChapterTimeStart")?.textContent)),
|
|
521
|
+
endTime: this.inputTimeToSeconds(String(chapter.querySelector("ChapterTimeEnd")?.textContent))
|
|
522
|
+
};
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
toString(pretty = false) {
|
|
526
|
+
const indent = indenter(pretty ? 2 : 0);
|
|
527
|
+
const output = [
|
|
528
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
529
|
+
'<!DOCTYPE Chapters SYSTEM "matroskachapters.dtd">',
|
|
530
|
+
"<Chapters>",
|
|
531
|
+
indent(1, "<EditionEntry>"),
|
|
532
|
+
indent(2, `<EditionUID>${Date.now()}${Int(Math.random() * 1e6)}</EditionUID>`)
|
|
533
|
+
];
|
|
534
|
+
this.chapters.forEach((chapter, index) => {
|
|
535
|
+
output.push(indent(2, "<ChapterAtom>"));
|
|
536
|
+
output.push(indent(3, `<ChapterTimeStart>${this.secondsToOutputTime(chapter.startTime)}</ChapterTimeStart>`));
|
|
537
|
+
output.push(indent(3, `<ChapterTimeEnd>${this.secondsToOutputTime(chapter.endTime)}</ChapterTimeEnd>`));
|
|
538
|
+
output.push(indent(3, `<ChapterUID>${Int(1 + chapter.startTime)}${Int(Math.random() * 1e6)}</ChapterUID>`));
|
|
539
|
+
output.push(indent(3, "<ChapterDisplay>"));
|
|
540
|
+
output.push(indent(4, `<${this.chapterStringNodeName}>${this.ensureTitle(index)}</${this.chapterStringNodeName}>`));
|
|
541
|
+
output.push(indent(3, "</ChapterDisplay>"));
|
|
542
|
+
output.push(indent(2, "</ChapterAtom>"));
|
|
543
|
+
});
|
|
544
|
+
output.push(
|
|
545
|
+
indent(1, "</EditionEntry>"),
|
|
546
|
+
"</Chapters>"
|
|
547
|
+
);
|
|
548
|
+
return output.join(pretty ? "\n" : "");
|
|
549
|
+
}
|
|
550
|
+
};
|
|
551
|
+
|
|
552
|
+
// src/Formats/AppleChapters.ts
|
|
553
|
+
var AppleChapters = class extends MatroskaXML {
|
|
554
|
+
supportsPrettyPrint = true;
|
|
555
|
+
filename = "apple-chapters.xml";
|
|
556
|
+
mimeType = "text/xml";
|
|
557
|
+
detect(inputString) {
|
|
558
|
+
return /^<\?xml/.test(inputString.trim()) && inputString.includes("<TextStream");
|
|
559
|
+
}
|
|
560
|
+
parse(string) {
|
|
561
|
+
if (!this.detect(string)) {
|
|
562
|
+
throw new Error("Input needs xml declaration and a <TextStream...> node");
|
|
563
|
+
}
|
|
564
|
+
let dom;
|
|
565
|
+
if (typeof DOMParser !== "undefined") {
|
|
566
|
+
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
567
|
+
} else {
|
|
568
|
+
dom = new JSDOM2(string, { contentType: "application/xml" });
|
|
569
|
+
dom = dom.window.document;
|
|
570
|
+
}
|
|
571
|
+
this.chapters = [...dom.querySelectorAll("TextSample")].map((chapter) => {
|
|
572
|
+
const title = String(chapter.getAttribute("text") ?? chapter.textContent);
|
|
573
|
+
return {
|
|
574
|
+
title,
|
|
575
|
+
startTime: timestampToSeconds(String(chapter.getAttribute("sampleTime")))
|
|
576
|
+
};
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
toString(pretty = false, exportOptions = {}) {
|
|
580
|
+
const indent = indenter(pretty ? 2 : 0);
|
|
581
|
+
const output = [
|
|
582
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
583
|
+
'<TextStream version="1.1">',
|
|
584
|
+
indent(1, "<TextStreamHeader>"),
|
|
585
|
+
indent(2, "<TextSampleDescription>"),
|
|
586
|
+
indent(2, "</TextSampleDescription>"),
|
|
587
|
+
indent(1, "</TextStreamHeader>")
|
|
588
|
+
];
|
|
589
|
+
this.chapters.forEach((chapter) => {
|
|
590
|
+
const attrContent = exportOptions.acUseTextAttr && chapter.title ? ` text="${chapter.title}"` : "";
|
|
591
|
+
const content = !exportOptions.acUseTextAttr && chapter.title ? chapter.title : "";
|
|
592
|
+
output.push(indent(3, `<TextSample sampleTime="${secondsToTimestamp(chapter.startTime, { milliseconds: true })}"${attrContent}>${content}</TextSample>`));
|
|
593
|
+
});
|
|
594
|
+
output.push(
|
|
595
|
+
"</TextStream>"
|
|
596
|
+
);
|
|
597
|
+
return output.join(pretty ? "\n" : "");
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
// src/Formats/ChaptersJson.ts
|
|
602
|
+
var ChaptersJson = class extends Base {
|
|
603
|
+
supportsPrettyPrint = true;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
// src/Formats/FFMetadata.ts
|
|
607
|
+
var FFMetadata = class extends Base {
|
|
608
|
+
filename = "FFMpegdata.txt";
|
|
609
|
+
mimeType = "text/plain";
|
|
610
|
+
characters = ["=", ";", "#", "\\", "\n"];
|
|
611
|
+
safeCharacters = this.characters.map((char) => escapeRegExpCharacters(char)).join("|");
|
|
612
|
+
unescapeRegexp = new RegExp("\\\\(" + this.safeCharacters + ")", "g");
|
|
613
|
+
escapeRegexp = new RegExp("(" + this.safeCharacters + ")", "g");
|
|
614
|
+
detect(inputString) {
|
|
615
|
+
return inputString.trim().slice(0, 12) === ";FFMETADATA1";
|
|
616
|
+
}
|
|
617
|
+
parse(string) {
|
|
618
|
+
if (!this.detect(string)) {
|
|
619
|
+
throw new Error(";FFMETADATA1 header missing :(");
|
|
620
|
+
}
|
|
621
|
+
const lines = stringToLines(string);
|
|
622
|
+
const chapters = [];
|
|
623
|
+
let ignoreAllUntilNextChapter = false;
|
|
624
|
+
let isMultilineTitle = false;
|
|
625
|
+
lines.forEach((line) => {
|
|
626
|
+
const [key, value] = line.split("=");
|
|
627
|
+
if (chapters.length === 0 && key === "title") {
|
|
628
|
+
this.meta.title = this.unescape(value);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (line === "[CHAPTER]") {
|
|
632
|
+
const c = { startTime: 0 };
|
|
633
|
+
chapters.push(c);
|
|
634
|
+
ignoreAllUntilNextChapter = false;
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
if (line.slice(0, 1) === "[") {
|
|
638
|
+
ignoreAllUntilNextChapter = true;
|
|
639
|
+
}
|
|
640
|
+
if (chapters.length === 0 || ignoreAllUntilNextChapter) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (!/[^\\]=/.test(line) && isMultilineTitle) {
|
|
644
|
+
chapters[chapters.length - 1].title += " " + line;
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
isMultilineTitle = false;
|
|
648
|
+
if (key === "title") {
|
|
649
|
+
chapters[chapters.length - 1].title = this.unescape(value);
|
|
650
|
+
if (/\\$/.test(value)) {
|
|
651
|
+
isMultilineTitle = true;
|
|
652
|
+
}
|
|
653
|
+
} else if (key === "START") {
|
|
654
|
+
chapters[chapters.length - 1].startTime = enforceMilliseconds(parseFloat(value) * 1e-3);
|
|
655
|
+
} else if (key === "END") {
|
|
656
|
+
chapters[chapters.length - 1].endTime = enforceMilliseconds(parseFloat(value) * 1e-3);
|
|
657
|
+
}
|
|
658
|
+
});
|
|
659
|
+
this.chapters = chapters;
|
|
660
|
+
}
|
|
661
|
+
unescape(string) {
|
|
662
|
+
return string.replace(this.unescapeRegexp, "$1").replace(/\\$/g, "");
|
|
663
|
+
}
|
|
664
|
+
escape(string) {
|
|
665
|
+
return string.replace(this.escapeRegexp, "\\$1");
|
|
666
|
+
}
|
|
667
|
+
toString() {
|
|
668
|
+
const output = [";FFMETADATA1"];
|
|
669
|
+
if (this.meta.title.trim().length > 0) {
|
|
670
|
+
output.push(`title=${this.escape(this.meta.title)}`);
|
|
671
|
+
}
|
|
672
|
+
output.push("");
|
|
673
|
+
this.chapters.forEach((chapter) => {
|
|
674
|
+
output.push("[CHAPTER]", "TIMEBASE=1/1000");
|
|
675
|
+
output.push("START=" + enforceMilliseconds(chapter.startTime) * 1e3);
|
|
676
|
+
output.push("END=" + enforceMilliseconds(chapter.endTime) * 1e3);
|
|
677
|
+
if (chapter.title && chapter.title.trim().length > 0) {
|
|
678
|
+
output.push(`title=${this.escape(chapter.title)}`);
|
|
679
|
+
}
|
|
680
|
+
output.push("");
|
|
681
|
+
});
|
|
682
|
+
return output.join("\n");
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
// src/Formats/FFMpegInfo.ts
|
|
687
|
+
var FFMpegInfo = class extends Base {
|
|
688
|
+
detect(inputString) {
|
|
689
|
+
return /^frame:\d/.test(inputString.trim());
|
|
690
|
+
}
|
|
691
|
+
parse(input) {
|
|
692
|
+
if (!this.detect(input)) {
|
|
693
|
+
throw new Error("input must start with frame:");
|
|
694
|
+
}
|
|
695
|
+
const matches = Array.from(input.matchAll(/frame:(\d+).*pts_time:([\d.]+)\r?\n/g));
|
|
696
|
+
this.chapters = matches.map((match) => {
|
|
697
|
+
const startTime = enforceMilliseconds(parseFloat(match[2]));
|
|
698
|
+
return {
|
|
699
|
+
startTime
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
this.rebuildChapterTitles();
|
|
703
|
+
}
|
|
704
|
+
toString() {
|
|
705
|
+
throw new Error("this class won't generate actual output");
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/Formats/MKVMergeSimple.ts
|
|
710
|
+
var MKVMergeSimple = class extends Base {
|
|
711
|
+
filename = "mkvmerge-chapters.txt";
|
|
712
|
+
mimeType = "text/plain";
|
|
713
|
+
zeroPad = 2;
|
|
714
|
+
detect(inputString) {
|
|
715
|
+
const re = new RegExp(`^CHAPTER${zeroPad(1, this.zeroPad)}`);
|
|
716
|
+
return re.test(inputString.trim());
|
|
717
|
+
}
|
|
718
|
+
parse(string) {
|
|
719
|
+
if (!this.detect(string)) {
|
|
720
|
+
throw new Error(`File must start with CHAPTER${zeroPad(1, this.zeroPad)}`);
|
|
721
|
+
}
|
|
722
|
+
const lines = string.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => line.trim());
|
|
723
|
+
const chapters = [];
|
|
724
|
+
lines.forEach((line) => {
|
|
725
|
+
const match = /^CHAPTER(?<index>\d+)(?<key>NAME)?=(?<value>.*)/.exec(line);
|
|
726
|
+
if (!match?.groups) {
|
|
727
|
+
return true;
|
|
728
|
+
}
|
|
729
|
+
const index = Int(match.groups.index) - 1;
|
|
730
|
+
const key = match.groups.key === "NAME" ? "title" : "startTime";
|
|
731
|
+
const value = key === "startTime" ? timestampToSeconds(match.groups.value) : match.groups.value;
|
|
732
|
+
if (chapters[index]) {
|
|
733
|
+
chapters[index][key] = value;
|
|
734
|
+
} else {
|
|
735
|
+
chapters[index] = { [key]: value };
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
this.chapters = chapters;
|
|
739
|
+
}
|
|
740
|
+
toString() {
|
|
741
|
+
return this.chapters.map((chapter, index) => {
|
|
742
|
+
const i = zeroPad(index + 1, this.zeroPad);
|
|
743
|
+
const options = {
|
|
744
|
+
hours: true,
|
|
745
|
+
milliseconds: true
|
|
746
|
+
};
|
|
747
|
+
const output = [
|
|
748
|
+
`CHAPTER${i}=${secondsToTimestamp(chapter.startTime, options)}`
|
|
749
|
+
];
|
|
750
|
+
if (chapter.title && chapter.title.trim().length > 0) {
|
|
751
|
+
output.push(`CHAPTER${i}NAME=${chapter.title}`);
|
|
752
|
+
}
|
|
753
|
+
return output.join("\n");
|
|
754
|
+
}).join("\n");
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/Formats/MKVMergeXML.ts
|
|
759
|
+
var MKVMergeXML = class extends MatroskaXML {
|
|
760
|
+
supportsPrettyPrint = true;
|
|
761
|
+
filename = "mkvmerge-chapters.xml";
|
|
762
|
+
mimeType = "text/xml";
|
|
763
|
+
chapterStringNodeName = "ChapterString";
|
|
764
|
+
inputTimeToSeconds(string) {
|
|
765
|
+
return timestampToSeconds(string);
|
|
766
|
+
}
|
|
767
|
+
secondsToOutputTime(seconds) {
|
|
768
|
+
return secondsToTimestamp(seconds, { hours: true, milliseconds: true });
|
|
769
|
+
}
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
// src/Formats/PySceneDetect.ts
|
|
773
|
+
var PySceneDetect = class extends Base {
|
|
774
|
+
filename = "psd-scenes.csv";
|
|
775
|
+
mimeType = "text/csv";
|
|
776
|
+
detect(inputString) {
|
|
777
|
+
return ["Scene Number", "Timecode Lis"].includes(inputString.trim().slice(0, 12));
|
|
778
|
+
}
|
|
779
|
+
parse(string) {
|
|
780
|
+
if (!this.detect(string)) {
|
|
781
|
+
throw new Error('File must start with "Scene Number" or "Timecode List"');
|
|
782
|
+
}
|
|
783
|
+
const lines = string.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => line.trim());
|
|
784
|
+
if (/^Timecode/.test(lines[0])) {
|
|
785
|
+
lines.shift();
|
|
786
|
+
}
|
|
787
|
+
lines.shift();
|
|
788
|
+
this.chapters = lines.map((line) => {
|
|
789
|
+
const cols = line.split(",");
|
|
790
|
+
return {
|
|
791
|
+
startTime: timestampToSeconds(cols[2]),
|
|
792
|
+
endTime: timestampToSeconds(cols[5])
|
|
793
|
+
};
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
toString(pretty = false, exportOptions = {}) {
|
|
797
|
+
const framerate = exportOptions.psdFramerate || 23.976;
|
|
798
|
+
const omitTimecodes = !!exportOptions.psdOmitTimecodes;
|
|
799
|
+
const lines = this.chapters.map((chapter, index) => {
|
|
800
|
+
const next = this.chapters[index + 1];
|
|
801
|
+
const endTime = next?.startTime || this.duration;
|
|
802
|
+
const l = endTime - chapter.startTime;
|
|
803
|
+
return [
|
|
804
|
+
index + 1,
|
|
805
|
+
// Scene Number
|
|
806
|
+
Math.round(chapter.startTime * framerate) + 1,
|
|
807
|
+
// Start Frame
|
|
808
|
+
secondsToTimestamp(chapter.startTime, { hours: true, milliseconds: true }),
|
|
809
|
+
// Start Timecode
|
|
810
|
+
Int(chapter.startTime * 1e3),
|
|
811
|
+
// Start Time (seconds)
|
|
812
|
+
Math.round(endTime * framerate),
|
|
813
|
+
// End Frame
|
|
814
|
+
secondsToTimestamp(endTime, { hours: true, milliseconds: true }),
|
|
815
|
+
// End Timecode
|
|
816
|
+
Int(endTime * 1e3),
|
|
817
|
+
// End Time (seconds)
|
|
818
|
+
Math.round((endTime - chapter.startTime) * framerate),
|
|
819
|
+
// Length (frames)
|
|
820
|
+
secondsToTimestamp(l, { hours: true, milliseconds: true }),
|
|
821
|
+
// Length (timecode)
|
|
822
|
+
Int(Math.ceil(l * 1e3))
|
|
823
|
+
// Length (seconds)
|
|
824
|
+
];
|
|
825
|
+
});
|
|
826
|
+
const tl = "Timecode List:" + lines.slice(1).map((l) => l[2]).join(",");
|
|
827
|
+
const outputLines = lines.map((l) => l.join(","));
|
|
828
|
+
outputLines.unshift("Scene Number,Start Frame,Start Timecode,Start Time (seconds),End Frame,End Timecode,End Time (seconds),Length (frames),Length (timecode),Length (seconds)");
|
|
829
|
+
if (!omitTimecodes) {
|
|
830
|
+
outputLines.unshift(tl);
|
|
831
|
+
}
|
|
832
|
+
return outputLines.join("\n");
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
// src/Formats/VorbisComment.ts
|
|
837
|
+
var VorbisComment = class extends MKVMergeSimple {
|
|
838
|
+
filename = "vorbis-comment.txt";
|
|
839
|
+
mimeType = "text/plain";
|
|
840
|
+
zeroPad = 3;
|
|
841
|
+
};
|
|
842
|
+
|
|
843
|
+
// src/Formats/WebVTT.ts
|
|
844
|
+
var WebVTT = class extends Base {
|
|
845
|
+
filename = "webvtt-chapters.vtt";
|
|
846
|
+
mimeType = "text/vtt";
|
|
847
|
+
detect(inputString) {
|
|
848
|
+
return inputString.trim().slice(0, 6) === "WEBVTT";
|
|
849
|
+
}
|
|
850
|
+
parse(string) {
|
|
851
|
+
if (!this.detect(string)) {
|
|
852
|
+
throw new Error("WEBVTT header missing :(");
|
|
853
|
+
}
|
|
854
|
+
const lines = string.split(/\r?\n/).filter((line) => line.trim().length > 0).map((line) => line.trim());
|
|
855
|
+
const header = lines.shift().split(/\s*-\s*/);
|
|
856
|
+
if (header[1]) {
|
|
857
|
+
this.meta.title = header[1];
|
|
858
|
+
}
|
|
859
|
+
const chapters = [];
|
|
860
|
+
lines.forEach((line) => {
|
|
861
|
+
if (/^\d+$/.test(line)) {
|
|
862
|
+
chapters.push({});
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
const index = chapters.length - 1;
|
|
866
|
+
const timestamps = /(.*)\s+-->\s+(.*)/.exec(line);
|
|
867
|
+
if (timestamps && timestamps.length === 3) {
|
|
868
|
+
chapters[index].startTime = timestampToSeconds(timestamps[1]);
|
|
869
|
+
chapters[index].endTime = timestampToSeconds(timestamps[2]);
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
chapters[index].title = line;
|
|
873
|
+
});
|
|
874
|
+
this.chapters = chapters;
|
|
875
|
+
}
|
|
876
|
+
toString() {
|
|
877
|
+
const output = ["WEBVTT"];
|
|
878
|
+
if (this.meta.title.trim().length > 0) {
|
|
879
|
+
output[0] += " - " + this.meta.title.trim();
|
|
880
|
+
}
|
|
881
|
+
const options = { hours: true, milliseconds: true };
|
|
882
|
+
this.chapters.forEach((chapter, index) => {
|
|
883
|
+
output.push("");
|
|
884
|
+
output.push(
|
|
885
|
+
...[
|
|
886
|
+
String(index + 1),
|
|
887
|
+
secondsToTimestamp(chapter.startTime, options) + " --> " + secondsToTimestamp(chapter.endTime, options),
|
|
888
|
+
this.ensureTitle(index)
|
|
889
|
+
].filter((line) => String(line).trim().length > 0)
|
|
890
|
+
);
|
|
891
|
+
});
|
|
892
|
+
return output.join("\n");
|
|
893
|
+
}
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// src/Formats/Youtube.ts
|
|
897
|
+
var Youtube = class extends Base {
|
|
898
|
+
filename = "youtube-chapters.txt";
|
|
899
|
+
mimeType = "text/plain";
|
|
900
|
+
detect(inputString) {
|
|
901
|
+
return /^0?0:00(:00)?\s/.test(inputString.trim());
|
|
902
|
+
}
|
|
903
|
+
parse(string) {
|
|
904
|
+
if (!this.detect(string)) {
|
|
905
|
+
throw new Error("Youtube Chapters *MUST* begin with (0)0:00(:00), received: " + string.substr(0, 10) + "...");
|
|
906
|
+
}
|
|
907
|
+
this.chapters = stringToLines(string).map((line) => {
|
|
908
|
+
const l = line.split(" ");
|
|
909
|
+
const timestamp = String(l.shift());
|
|
910
|
+
return {
|
|
911
|
+
startTime: timestampToSeconds(timestamp),
|
|
912
|
+
title: l.join(" ")
|
|
913
|
+
};
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
toString() {
|
|
917
|
+
const options = {
|
|
918
|
+
milliseconds: false,
|
|
919
|
+
hours: this.chapters.at(-1).startTime > 3600
|
|
920
|
+
};
|
|
921
|
+
return this.chapters.map((chapter, index) => {
|
|
922
|
+
const startTime = index === 0 && chapter.startTime !== 0 ? 0 : chapter.startTime;
|
|
923
|
+
return `${secondsToTimestamp(startTime, options)} ${this.ensureTitle(index)}`;
|
|
924
|
+
}).join("\n");
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
|
|
928
|
+
// src/Formats/ShutterEDL.ts
|
|
929
|
+
var ShutterEDL = class extends Base {
|
|
930
|
+
// this format is based on the shutter encoder edl format
|
|
931
|
+
// https://github.com/paulpacifico/shutter-encoder/blob/f3d6bb6dfcd629861a0b0a50113bf4b062e1ba17/src/application/SceneDetection.java
|
|
932
|
+
detect(inputString) {
|
|
933
|
+
return /^TITLE:\s.*\r?\n/.test(inputString.trim());
|
|
934
|
+
}
|
|
935
|
+
decodeTime(timeString) {
|
|
936
|
+
return timeString.replace(/:(\d+)$/, ".$10");
|
|
937
|
+
}
|
|
938
|
+
encodeTime(time) {
|
|
939
|
+
const string = secondsToTimestamp(time, { milliseconds: true });
|
|
940
|
+
const ms = String(Math.ceil(Int(string.split(".").pop()) * 0.1));
|
|
941
|
+
return string.replace(/\.(\d+)$/, `:${ms.padStart(2, "0")}`);
|
|
942
|
+
}
|
|
943
|
+
parse(input) {
|
|
944
|
+
if (!this.detect(input)) {
|
|
945
|
+
throw new Error("input must start with TITLE:");
|
|
946
|
+
}
|
|
947
|
+
const titleMatch = input.match(/^TITLE:\s(.*)\r?\n/);
|
|
948
|
+
this.meta.title = titleMatch?.[1] ?? "Chapters";
|
|
949
|
+
this.chapters = Array.from(input.matchAll(/(?<index>\d{6})\s+(?<title>[^\s]+)\s+\w+\s+\w+\s+(?<startTime>\d\d:\d\d:\d\d:\d\d)\s+(?<endTime>\d\d:\d\d:\d\d:\d\d)/g)).reduce((acc, match) => {
|
|
950
|
+
if (!match?.groups) {
|
|
951
|
+
return acc;
|
|
952
|
+
}
|
|
953
|
+
const startTime = timestampToSeconds(this.decodeTime(match.groups.startTime));
|
|
954
|
+
const endTime = timestampToSeconds(this.decodeTime(match.groups.endTime));
|
|
955
|
+
const title = match.groups.title;
|
|
956
|
+
const last = acc.at(-1);
|
|
957
|
+
if (last?.startTime === startTime) {
|
|
958
|
+
return acc;
|
|
959
|
+
}
|
|
960
|
+
acc.push({
|
|
961
|
+
startTime,
|
|
962
|
+
endTime,
|
|
963
|
+
title
|
|
964
|
+
});
|
|
965
|
+
return acc;
|
|
966
|
+
}, []);
|
|
967
|
+
}
|
|
968
|
+
toString() {
|
|
969
|
+
const tracks = ["V", "A", "A2"];
|
|
970
|
+
const output = this.chapters.reduce((acc, chapter, i) => {
|
|
971
|
+
const index = i * 3 + 1;
|
|
972
|
+
const startTime = this.encodeTime(chapter.startTime);
|
|
973
|
+
const endTime = this.encodeTime(chapter.endTime);
|
|
974
|
+
for (let j = 0; j < 3; j++) {
|
|
975
|
+
acc.push(`${(j + index).toString().padStart(6, "0")} ${chapter.title} ${tracks[j]}${" ".repeat(6 - tracks[j].length)}C ${startTime} ${endTime} ${startTime} ${endTime}`);
|
|
976
|
+
}
|
|
977
|
+
return acc;
|
|
978
|
+
}, []);
|
|
979
|
+
output.unshift("TITLE: " + (this.meta.title ?? "Chapters"));
|
|
980
|
+
return output.join("\n");
|
|
981
|
+
}
|
|
982
|
+
};
|
|
983
|
+
|
|
984
|
+
// src/Formats/PodloveSimpleChapters.ts
|
|
985
|
+
import { JSDOM as JSDOM3 } from "jsdom";
|
|
986
|
+
var PodloveSimpleChapters = class extends Base {
|
|
987
|
+
supportsPrettyPrint = true;
|
|
988
|
+
filename = "podlove-simple-chapters-fragment.xml";
|
|
989
|
+
mimeType = "text/xml";
|
|
990
|
+
detect(inputString) {
|
|
991
|
+
return inputString.includes("<psc:chapters");
|
|
992
|
+
}
|
|
993
|
+
parse(string) {
|
|
994
|
+
if (!this.detect(string)) {
|
|
995
|
+
throw new Error("Input must contain <psc:chapters ...> node");
|
|
996
|
+
}
|
|
997
|
+
let dom;
|
|
998
|
+
if (typeof DOMParser !== "undefined") {
|
|
999
|
+
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
1000
|
+
} else {
|
|
1001
|
+
dom = new JSDOM3(string, { contentType: "application/xml" });
|
|
1002
|
+
dom = dom.window.document;
|
|
1003
|
+
}
|
|
1004
|
+
this.chapters = [...dom.querySelectorAll("[start]")].reduce((acc, node) => {
|
|
1005
|
+
if (node.tagName === "psc:chapter") {
|
|
1006
|
+
const start = node.getAttribute("start");
|
|
1007
|
+
const title = node.getAttribute("title");
|
|
1008
|
+
const image = node.getAttribute("image");
|
|
1009
|
+
const href = node.getAttribute("href");
|
|
1010
|
+
const chapter = {
|
|
1011
|
+
startTime: NPTToSeconds(start)
|
|
1012
|
+
};
|
|
1013
|
+
if (title) {
|
|
1014
|
+
chapter.title = title;
|
|
1015
|
+
}
|
|
1016
|
+
if (image) {
|
|
1017
|
+
chapter.img = image;
|
|
1018
|
+
}
|
|
1019
|
+
if (href) {
|
|
1020
|
+
chapter.url = href;
|
|
1021
|
+
}
|
|
1022
|
+
acc.push(chapter);
|
|
1023
|
+
}
|
|
1024
|
+
return acc;
|
|
1025
|
+
}, []);
|
|
1026
|
+
}
|
|
1027
|
+
toString(pretty = false) {
|
|
1028
|
+
const indent = indenter(pretty ? 2 : 0);
|
|
1029
|
+
const output = [
|
|
1030
|
+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
1031
|
+
indent(1, "<channel>"),
|
|
1032
|
+
indent(2, "<!-- this is only a fragment of an rss feed, see -->"),
|
|
1033
|
+
indent(2, "<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->"),
|
|
1034
|
+
indent(2, "<!-- for more information -->"),
|
|
1035
|
+
indent(2, '<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">')
|
|
1036
|
+
];
|
|
1037
|
+
this.chapters.forEach((chapter) => {
|
|
1038
|
+
const node = [
|
|
1039
|
+
`<psc:chapter start="${secondsToNPT(chapter.startTime)}"`
|
|
1040
|
+
];
|
|
1041
|
+
if (chapter.title) {
|
|
1042
|
+
node.push(` title="${chapter.title}"`);
|
|
1043
|
+
}
|
|
1044
|
+
if (chapter.img) {
|
|
1045
|
+
node.push(` image="${chapter.img}"`);
|
|
1046
|
+
}
|
|
1047
|
+
if (chapter.url) {
|
|
1048
|
+
node.push(` href="${chapter.url}"`);
|
|
1049
|
+
}
|
|
1050
|
+
node.push("/>");
|
|
1051
|
+
output.push(indent(3, node.join("")));
|
|
1052
|
+
});
|
|
1053
|
+
output.push(
|
|
1054
|
+
indent(2, "</psc:chapters>"),
|
|
1055
|
+
indent(1, "</channel>"),
|
|
1056
|
+
indent(0, "</rss>")
|
|
1057
|
+
);
|
|
1058
|
+
return output.join(pretty ? "\n" : "");
|
|
1059
|
+
}
|
|
1060
|
+
};
|
|
1061
|
+
|
|
1062
|
+
// src/Formats/MP4Chaps.ts
|
|
1063
|
+
var MP4Chaps = class extends Base {
|
|
1064
|
+
filename = "mp4chaps.txt";
|
|
1065
|
+
mimeType = "text/plain";
|
|
1066
|
+
detect(inputString) {
|
|
1067
|
+
return /^\d\d:\d\d:\d\d.\d\d?\d?\s/.test(inputString.trim());
|
|
1068
|
+
}
|
|
1069
|
+
parse(string) {
|
|
1070
|
+
if (!this.detect(string)) {
|
|
1071
|
+
throw new Error("MP4Chaps *MUST* begin with 00:00:00, received: " + string.substr(0, 10) + "...");
|
|
1072
|
+
}
|
|
1073
|
+
this.chapters = stringToLines(string).map((line) => {
|
|
1074
|
+
const l = line.split(" ");
|
|
1075
|
+
const startTime = timestampToSeconds(l.shift());
|
|
1076
|
+
const [title, href] = l.join(" ").split("<");
|
|
1077
|
+
const chapter = {
|
|
1078
|
+
startTime,
|
|
1079
|
+
title: title.trim()
|
|
1080
|
+
};
|
|
1081
|
+
if (href) {
|
|
1082
|
+
chapter.url = href.replace(">", "");
|
|
1083
|
+
}
|
|
1084
|
+
return chapter;
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
toString() {
|
|
1088
|
+
return this.chapters.map((chapter) => {
|
|
1089
|
+
const line = [];
|
|
1090
|
+
line.push(secondsToTimestamp(chapter.startTime, { milliseconds: true }));
|
|
1091
|
+
line.push(chapter.title);
|
|
1092
|
+
if (chapter.url) {
|
|
1093
|
+
line.push(`<${chapter.url}>`);
|
|
1094
|
+
}
|
|
1095
|
+
return line.join(" ");
|
|
1096
|
+
}).join("\n");
|
|
1097
|
+
}
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
// src/Formats/PodloveJson.ts
|
|
1101
|
+
var PodloveJson = class extends Base {
|
|
1102
|
+
filename = "podlove-chapters.json";
|
|
1103
|
+
mimeType = "application/json";
|
|
1104
|
+
test(data) {
|
|
1105
|
+
if (!Array.isArray(data)) {
|
|
1106
|
+
return { errors: ["JSON Structure: must be an array"] };
|
|
1107
|
+
}
|
|
1108
|
+
if (data.length === 0) {
|
|
1109
|
+
return { errors: ["JSON Structure: must not be empty"] };
|
|
1110
|
+
}
|
|
1111
|
+
if (!data.every((chapter) => "start" in chapter)) {
|
|
1112
|
+
return { errors: ["JSON Structure: every chapter must have a start property"] };
|
|
1113
|
+
}
|
|
1114
|
+
return { errors: [] };
|
|
1115
|
+
}
|
|
1116
|
+
parse(string) {
|
|
1117
|
+
const data = JSON.parse(string);
|
|
1118
|
+
const { errors } = this.test(data);
|
|
1119
|
+
if (errors.length > 0) {
|
|
1120
|
+
throw new Error(errors.join(""));
|
|
1121
|
+
}
|
|
1122
|
+
this.chapters = data.map((raw) => {
|
|
1123
|
+
const { start, title, image, href } = raw;
|
|
1124
|
+
const chapter = {
|
|
1125
|
+
startTime: timestampToSeconds(start)
|
|
1126
|
+
};
|
|
1127
|
+
if (title) {
|
|
1128
|
+
chapter.title = title;
|
|
1129
|
+
}
|
|
1130
|
+
if (image) {
|
|
1131
|
+
chapter.img = image;
|
|
1132
|
+
}
|
|
1133
|
+
if (href) {
|
|
1134
|
+
chapter.url = href;
|
|
1135
|
+
}
|
|
1136
|
+
return chapter;
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
toString(pretty = false) {
|
|
1140
|
+
return JSON.stringify(this.chapters.map((chapter, i) => ({
|
|
1141
|
+
start: secondsToTimestamp(chapter.startTime, { milliseconds: true }),
|
|
1142
|
+
title: this.ensureTitle(i),
|
|
1143
|
+
image: chapter.img ?? "",
|
|
1144
|
+
href: chapter.url ?? ""
|
|
1145
|
+
})), null, pretty ? 2 : 0);
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
// src/Formats/AppleHLS.ts
|
|
1150
|
+
var AppleHLS = class extends Base {
|
|
1151
|
+
filename = "apple-hls.json";
|
|
1152
|
+
mimeType = "application/json";
|
|
1153
|
+
supportsPrettyPrint = true;
|
|
1154
|
+
titleLanguage = "en";
|
|
1155
|
+
imageDims = [1280, 720];
|
|
1156
|
+
test(data) {
|
|
1157
|
+
if (!Array.isArray(data)) {
|
|
1158
|
+
return { errors: ["JSON Structure: must be an array"] };
|
|
1159
|
+
}
|
|
1160
|
+
if (data.length === 0) {
|
|
1161
|
+
return { errors: ["JSON Structure: must not be empty"] };
|
|
1162
|
+
}
|
|
1163
|
+
if (!data.every((chapter) => "chapter" in chapter && "start-time" in chapter)) {
|
|
1164
|
+
return { errors: ["JSON Structure: every chapter must have a chapter and a start-time property"] };
|
|
1165
|
+
}
|
|
1166
|
+
return { errors: [] };
|
|
1167
|
+
}
|
|
1168
|
+
parse(string) {
|
|
1169
|
+
const data = JSON.parse(string);
|
|
1170
|
+
const { errors } = this.test(data);
|
|
1171
|
+
if (errors.length > 0) {
|
|
1172
|
+
throw new Error(errors.join(""));
|
|
1173
|
+
}
|
|
1174
|
+
this.chapters = data.map((raw) => {
|
|
1175
|
+
const chapter = {
|
|
1176
|
+
startTime: parseFloat(raw["start-time"])
|
|
1177
|
+
};
|
|
1178
|
+
if ("titles" in raw && raw.titles.length > 0) {
|
|
1179
|
+
chapter.title = raw.titles[0].title;
|
|
1180
|
+
}
|
|
1181
|
+
if ("images" in raw && raw.images.length > 0) {
|
|
1182
|
+
chapter.img = raw.images[0].url;
|
|
1183
|
+
}
|
|
1184
|
+
return chapter;
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
toString(pretty = false) {
|
|
1188
|
+
return JSON.stringify(this.chapters.map((c, i) => {
|
|
1189
|
+
const chapter = {
|
|
1190
|
+
"start-time": c.startTime,
|
|
1191
|
+
chapter: i + 1,
|
|
1192
|
+
titles: [
|
|
1193
|
+
{
|
|
1194
|
+
title: this.ensureTitle(i),
|
|
1195
|
+
language: this.titleLanguage
|
|
1196
|
+
}
|
|
1197
|
+
]
|
|
1198
|
+
};
|
|
1199
|
+
if (c.img) {
|
|
1200
|
+
chapter.images = [
|
|
1201
|
+
{
|
|
1202
|
+
"image-category": "chapter",
|
|
1203
|
+
url: c.img,
|
|
1204
|
+
"pixel-width": this.imageDims[0],
|
|
1205
|
+
"pixel-height": this.imageDims[1]
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
}
|
|
1209
|
+
return chapter;
|
|
1210
|
+
}), null, pretty ? 2 : 0);
|
|
1211
|
+
}
|
|
1212
|
+
};
|
|
1213
|
+
|
|
1214
|
+
// src/Formats/Scenecut.ts
|
|
1215
|
+
var Scenecut = class extends Base {
|
|
1216
|
+
filename = "scene-cuts.json";
|
|
1217
|
+
mimeType = "application/json";
|
|
1218
|
+
frameRate = 30;
|
|
1219
|
+
/**
|
|
1220
|
+
* The number "1001" in the context of video processing and multimedia, especially in relation to frame rates and timecodes, is often associated with the NTSC color system used in North America and Japan.
|
|
1221
|
+
* In this system, the frame rate is often described as "29.97 frames per second", but it's technically 30000/1001 frames per second, which is approximately 29.97 but not exactly. This is known as a "drop frame" rate, and the "1001" comes from this fractional frame rate.
|
|
1222
|
+
*/
|
|
1223
|
+
ptsScale = 1;
|
|
1224
|
+
score = 0.5;
|
|
1225
|
+
test(data) {
|
|
1226
|
+
if (!Array.isArray(data)) {
|
|
1227
|
+
return { errors: ["JSON Structure: must be an array"] };
|
|
1228
|
+
}
|
|
1229
|
+
if (data.length === 0) {
|
|
1230
|
+
return { errors: ["JSON Structure: must not be empty"] };
|
|
1231
|
+
}
|
|
1232
|
+
if (!data.every((chapter) => "pts_time" in chapter)) {
|
|
1233
|
+
return { errors: ["JSON Structure: every chapter must have a start property"] };
|
|
1234
|
+
}
|
|
1235
|
+
return { errors: [] };
|
|
1236
|
+
}
|
|
1237
|
+
parse(string) {
|
|
1238
|
+
const data = JSON.parse(string);
|
|
1239
|
+
const { errors } = this.test(data);
|
|
1240
|
+
if (errors.length > 0) {
|
|
1241
|
+
throw new Error(errors.join(""));
|
|
1242
|
+
}
|
|
1243
|
+
this.chapters = data.map((raw) => {
|
|
1244
|
+
const chapter = {
|
|
1245
|
+
startTime: Float(raw.pts_time)
|
|
1246
|
+
};
|
|
1247
|
+
this.frameRate = raw.pts / raw.pts_time;
|
|
1248
|
+
return chapter;
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
toString(pretty = false, exportOptions = {}) {
|
|
1252
|
+
const frameRate = Float("frameRate" in exportOptions ? exportOptions.frameRate : this.frameRate);
|
|
1253
|
+
const ptsScale = Float("ptsScale" in exportOptions ? exportOptions.ptsScale : this.ptsScale);
|
|
1254
|
+
const score = Float("score" in exportOptions ? exportOptions.score : this.score);
|
|
1255
|
+
return JSON.stringify(this.chapters.map((chapter) => ({
|
|
1256
|
+
frame: Math.round(chapter.startTime * frameRate),
|
|
1257
|
+
pts: Math.round(chapter.startTime * frameRate * ptsScale).toFixed(3),
|
|
1258
|
+
pts_time: chapter.startTime,
|
|
1259
|
+
score
|
|
1260
|
+
})), null, pretty ? 2 : 0);
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
|
|
1264
|
+
// src/Formats/AutoFormat.ts
|
|
1265
|
+
var classMap = {
|
|
1266
|
+
chaptersjson: ChaptersJson,
|
|
1267
|
+
ffmetadata: FFMetadata,
|
|
1268
|
+
matroskaxml: MatroskaXML,
|
|
1269
|
+
mkvmergexml: MKVMergeXML,
|
|
1270
|
+
mkvmergesimple: MKVMergeSimple,
|
|
1271
|
+
webvtt: WebVTT,
|
|
1272
|
+
youtube: Youtube,
|
|
1273
|
+
ffmpeginfo: FFMpegInfo,
|
|
1274
|
+
pyscenedetect: PySceneDetect,
|
|
1275
|
+
vorbiscomment: VorbisComment,
|
|
1276
|
+
applechapters: AppleChapters,
|
|
1277
|
+
shutteredl: ShutterEDL,
|
|
1278
|
+
psc: PodloveSimpleChapters,
|
|
1279
|
+
mp4chaps: MP4Chaps,
|
|
1280
|
+
podlovejson: PodloveJson,
|
|
1281
|
+
applehls: AppleHLS,
|
|
1282
|
+
scenecut: Scenecut
|
|
1283
|
+
};
|
|
1284
|
+
var AutoFormat = {
|
|
1285
|
+
classMap,
|
|
1286
|
+
detect(inputString, returnWhat = "instance") {
|
|
1287
|
+
let detected;
|
|
1288
|
+
for (const [key, className] of Object.entries(this.classMap)) {
|
|
1289
|
+
try {
|
|
1290
|
+
detected = className.create(inputString);
|
|
1291
|
+
if (detected) {
|
|
1292
|
+
if (returnWhat === "class") {
|
|
1293
|
+
return className;
|
|
1294
|
+
} else if (returnWhat === "key") {
|
|
1295
|
+
return key;
|
|
1296
|
+
}
|
|
1297
|
+
return detected;
|
|
1298
|
+
}
|
|
1299
|
+
} catch (e) {
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
if (!detected) {
|
|
1303
|
+
throw new Error("failed to detect type of given input :(");
|
|
1304
|
+
}
|
|
1305
|
+
},
|
|
1306
|
+
from(inputString) {
|
|
1307
|
+
return this.detect(inputString);
|
|
1308
|
+
},
|
|
1309
|
+
as(classKeyOrClass, input) {
|
|
1310
|
+
if (typeof classKeyOrClass === "string") {
|
|
1311
|
+
if (!(classKeyOrClass in this.classMap)) {
|
|
1312
|
+
throw new Error(`invalid class key "${classKeyOrClass}"`);
|
|
1313
|
+
}
|
|
1314
|
+
return this.classMap[classKeyOrClass].create(input);
|
|
1315
|
+
}
|
|
1316
|
+
return classKeyOrClass.create(input);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
export {
|
|
1320
|
+
AppleChapters,
|
|
1321
|
+
AppleHLS,
|
|
1322
|
+
AutoFormat,
|
|
1323
|
+
ChaptersJson,
|
|
1324
|
+
FFMetadata,
|
|
1325
|
+
FFMpegInfo,
|
|
1326
|
+
MKVMergeSimple,
|
|
1327
|
+
MKVMergeXML,
|
|
1328
|
+
MP4Chaps,
|
|
1329
|
+
MatroskaXML,
|
|
1330
|
+
PodloveJson,
|
|
1331
|
+
PodloveSimpleChapters,
|
|
1332
|
+
PySceneDetect,
|
|
1333
|
+
Scenecut,
|
|
1334
|
+
ShutterEDL,
|
|
1335
|
+
util_exports as Util,
|
|
1336
|
+
VorbisComment,
|
|
1337
|
+
WebVTT,
|
|
1338
|
+
Youtube
|
|
1339
|
+
};
|