@mtillmann/chapters 0.1.5 → 0.1.7
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.js +210 -54
- package/package.json +3 -2
- package/readme.md +1 -0
package/dist/index.js
CHANGED
|
@@ -4,9 +4,6 @@ var __export = (target, all) => {
|
|
|
4
4
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
5
|
};
|
|
6
6
|
|
|
7
|
-
// src/Formats/AppleChapters.ts
|
|
8
|
-
import { JSDOM as JSDOM2 } from "jsdom";
|
|
9
|
-
|
|
10
7
|
// src/util.ts
|
|
11
8
|
var util_exports = {};
|
|
12
9
|
__export(util_exports, {
|
|
@@ -488,7 +485,7 @@ var Base = class {
|
|
|
488
485
|
};
|
|
489
486
|
|
|
490
487
|
// src/Formats/MatroskaXML.ts
|
|
491
|
-
import {
|
|
488
|
+
import { XMLParser } from "fast-xml-parser";
|
|
492
489
|
var MatroskaXML = class extends Base {
|
|
493
490
|
supportsPrettyPrint = true;
|
|
494
491
|
filename = "matroska-chapters.xml";
|
|
@@ -507,20 +504,29 @@ var MatroskaXML = class extends Base {
|
|
|
507
504
|
if (!this.detect(string)) {
|
|
508
505
|
throw new Error("Input needs xml declaration and a <Chapters> node");
|
|
509
506
|
}
|
|
510
|
-
let dom;
|
|
511
507
|
if (typeof DOMParser !== "undefined") {
|
|
512
|
-
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
508
|
+
const dom = new DOMParser().parseFromString(string, "application/xml");
|
|
509
|
+
this.chapters = [...dom.querySelectorAll("ChapterAtom")].map((chapter) => {
|
|
510
|
+
return {
|
|
511
|
+
title: String(chapter.querySelector(this.chapterStringNodeName)?.textContent),
|
|
512
|
+
startTime: this.inputTimeToSeconds(String(chapter.querySelector("ChapterTimeStart")?.textContent)),
|
|
513
|
+
endTime: this.inputTimeToSeconds(String(chapter.querySelector("ChapterTimeEnd")?.textContent))
|
|
514
|
+
};
|
|
515
|
+
});
|
|
513
516
|
} else {
|
|
514
|
-
|
|
515
|
-
|
|
517
|
+
const parsed = new XMLParser({
|
|
518
|
+
ignoreAttributes: false,
|
|
519
|
+
attributeNamePrefix: "@_"
|
|
520
|
+
}).parse(string);
|
|
521
|
+
this.chapters = parsed.Chapters.EditionEntry.ChapterAtom.map((chapter) => {
|
|
522
|
+
const title = chapter.ChapterDisplay[this.chapterStringNodeName];
|
|
523
|
+
return {
|
|
524
|
+
title,
|
|
525
|
+
startTime: this.inputTimeToSeconds(String(chapter.ChapterTimeStart)),
|
|
526
|
+
endTime: this.inputTimeToSeconds(String(chapter.ChapterTimeEnd))
|
|
527
|
+
};
|
|
528
|
+
});
|
|
516
529
|
}
|
|
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
530
|
}
|
|
525
531
|
toString(pretty = false) {
|
|
526
532
|
const indent = indenter(pretty ? 2 : 0);
|
|
@@ -550,6 +556,7 @@ var MatroskaXML = class extends Base {
|
|
|
550
556
|
};
|
|
551
557
|
|
|
552
558
|
// src/Formats/AppleChapters.ts
|
|
559
|
+
import { XMLParser as XMLParser2 } from "fast-xml-parser";
|
|
553
560
|
var AppleChapters = class extends MatroskaXML {
|
|
554
561
|
supportsPrettyPrint = true;
|
|
555
562
|
filename = "apple-chapters.xml";
|
|
@@ -561,20 +568,28 @@ var AppleChapters = class extends MatroskaXML {
|
|
|
561
568
|
if (!this.detect(string)) {
|
|
562
569
|
throw new Error("Input needs xml declaration and a <TextStream...> node");
|
|
563
570
|
}
|
|
564
|
-
let dom;
|
|
565
571
|
if (typeof DOMParser !== "undefined") {
|
|
566
|
-
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
572
|
+
const dom = new DOMParser().parseFromString(string, "application/xml");
|
|
573
|
+
this.chapters = [...dom.querySelectorAll("TextSample")].map((chapter) => {
|
|
574
|
+
const title = String(chapter.getAttribute("text") ?? chapter.textContent);
|
|
575
|
+
return {
|
|
576
|
+
title,
|
|
577
|
+
startTime: timestampToSeconds(String(chapter.getAttribute("sampleTime")))
|
|
578
|
+
};
|
|
579
|
+
});
|
|
567
580
|
} else {
|
|
568
|
-
|
|
569
|
-
|
|
581
|
+
const parsed = new XMLParser2({
|
|
582
|
+
ignoreAttributes: false,
|
|
583
|
+
attributeNamePrefix: "@_"
|
|
584
|
+
}).parse(string);
|
|
585
|
+
this.chapters = parsed.TextStream.TextSample.map((chapter) => {
|
|
586
|
+
const title = chapter["#text"] || chapter["@_text"];
|
|
587
|
+
return {
|
|
588
|
+
title,
|
|
589
|
+
startTime: timestampToSeconds(chapter["@_sampleTime"])
|
|
590
|
+
};
|
|
591
|
+
});
|
|
570
592
|
}
|
|
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
593
|
}
|
|
579
594
|
toString(pretty = false, exportOptions = {}) {
|
|
580
595
|
const indent = indenter(pretty ? 2 : 0);
|
|
@@ -983,7 +998,7 @@ var ShutterEDL = class extends Base {
|
|
|
983
998
|
};
|
|
984
999
|
|
|
985
1000
|
// src/Formats/PodloveSimpleChapters.ts
|
|
986
|
-
import {
|
|
1001
|
+
import { XMLParser as XMLParser3 } from "fast-xml-parser";
|
|
987
1002
|
var PodloveSimpleChapters = class extends Base {
|
|
988
1003
|
supportsPrettyPrint = true;
|
|
989
1004
|
filename = "podlove-simple-chapters-fragment.xml";
|
|
@@ -995,45 +1010,64 @@ var PodloveSimpleChapters = class extends Base {
|
|
|
995
1010
|
if (!this.detect(string)) {
|
|
996
1011
|
throw new Error("Input must contain <psc:chapters ...> node");
|
|
997
1012
|
}
|
|
998
|
-
let dom;
|
|
999
1013
|
if (typeof DOMParser !== "undefined") {
|
|
1000
|
-
dom = new DOMParser().parseFromString(string, "application/xml");
|
|
1014
|
+
const dom = new DOMParser().parseFromString(string, "application/xml");
|
|
1015
|
+
this.chapters = [...dom.querySelectorAll("[start]")].reduce((acc, node) => {
|
|
1016
|
+
if (node.tagName === "psc:chapter") {
|
|
1017
|
+
const start = node.getAttribute("start");
|
|
1018
|
+
const title = node.getAttribute("title");
|
|
1019
|
+
const image = node.getAttribute("image");
|
|
1020
|
+
const href = node.getAttribute("href");
|
|
1021
|
+
const chapter = {
|
|
1022
|
+
startTime: NPTToSeconds(start)
|
|
1023
|
+
};
|
|
1024
|
+
if (title) {
|
|
1025
|
+
chapter.title = title;
|
|
1026
|
+
}
|
|
1027
|
+
if (image) {
|
|
1028
|
+
chapter.img = image;
|
|
1029
|
+
}
|
|
1030
|
+
if (href) {
|
|
1031
|
+
chapter.url = href;
|
|
1032
|
+
}
|
|
1033
|
+
acc.push(chapter);
|
|
1034
|
+
}
|
|
1035
|
+
return acc;
|
|
1036
|
+
}, []);
|
|
1001
1037
|
} else {
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
const image = node.getAttribute("image");
|
|
1010
|
-
const href = node.getAttribute("href");
|
|
1011
|
-
const chapter = {
|
|
1012
|
-
startTime: NPTToSeconds(start)
|
|
1038
|
+
const parsed = new XMLParser3({
|
|
1039
|
+
ignoreAttributes: false,
|
|
1040
|
+
attributeNamePrefix: "@_"
|
|
1041
|
+
}).parse(string);
|
|
1042
|
+
this.chapters = parsed.rss.channel.item["psc:chapters"]["psc:chapter"].map((chapter) => {
|
|
1043
|
+
const item = {
|
|
1044
|
+
startTime: NPTToSeconds(chapter["@_start"])
|
|
1013
1045
|
};
|
|
1014
|
-
if (
|
|
1015
|
-
|
|
1046
|
+
if (chapter["@_title"]) {
|
|
1047
|
+
item.title = chapter["@_title"];
|
|
1016
1048
|
}
|
|
1017
|
-
if (
|
|
1018
|
-
|
|
1049
|
+
if (chapter["@_image"]) {
|
|
1050
|
+
item.img = chapter["@_image"];
|
|
1019
1051
|
}
|
|
1020
|
-
if (
|
|
1021
|
-
|
|
1052
|
+
if (chapter["@_href"]) {
|
|
1053
|
+
item.url = chapter["@_href"];
|
|
1022
1054
|
}
|
|
1023
|
-
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
}, []);
|
|
1055
|
+
return item;
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1027
1058
|
}
|
|
1028
1059
|
toString(pretty = false) {
|
|
1029
1060
|
const indent = indenter(pretty ? 2 : 0);
|
|
1030
1061
|
const output = [
|
|
1062
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1031
1063
|
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
|
|
1032
1064
|
indent(1, "<channel>"),
|
|
1033
1065
|
indent(2, "<!-- this is only a fragment of an rss feed, see -->"),
|
|
1034
1066
|
indent(2, "<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->"),
|
|
1035
1067
|
indent(2, "<!-- for more information -->"),
|
|
1036
|
-
indent(2, '<
|
|
1068
|
+
indent(2, '<atom:link type="text/html" href="http://podlove.org/" />'),
|
|
1069
|
+
indent(2, "<item>"),
|
|
1070
|
+
indent(3, '<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">')
|
|
1037
1071
|
];
|
|
1038
1072
|
this.chapters.forEach((chapter) => {
|
|
1039
1073
|
const node = [
|
|
@@ -1049,10 +1083,11 @@ var PodloveSimpleChapters = class extends Base {
|
|
|
1049
1083
|
node.push(` href="${chapter.url}"`);
|
|
1050
1084
|
}
|
|
1051
1085
|
node.push("/>");
|
|
1052
|
-
output.push(indent(
|
|
1086
|
+
output.push(indent(4, node.join("")));
|
|
1053
1087
|
});
|
|
1054
1088
|
output.push(
|
|
1055
|
-
indent(
|
|
1089
|
+
indent(3, "</psc:chapters>"),
|
|
1090
|
+
indent(2, "</item>"),
|
|
1056
1091
|
indent(1, "</channel>"),
|
|
1057
1092
|
indent(0, "</rss>")
|
|
1058
1093
|
);
|
|
@@ -1263,6 +1298,125 @@ var Scenecut = class extends Base {
|
|
|
1263
1298
|
}
|
|
1264
1299
|
};
|
|
1265
1300
|
|
|
1301
|
+
// src/Formats/Audible.ts
|
|
1302
|
+
var Audible = class extends Base {
|
|
1303
|
+
filename = "audible-chapters.json";
|
|
1304
|
+
mimeType = "application/json";
|
|
1305
|
+
test(data) {
|
|
1306
|
+
if (!("content_license" in data)) {
|
|
1307
|
+
return { errors: ["JSON Structure: key content_license missing"] };
|
|
1308
|
+
}
|
|
1309
|
+
if (!("content_metadata" in data.content_license)) {
|
|
1310
|
+
return { errors: ["JSON Structure: key content_license.content_metadata missing"] };
|
|
1311
|
+
}
|
|
1312
|
+
if (!("chapter_info" in data.content_license.content_metadata)) {
|
|
1313
|
+
return { errors: ["JSON Structure: key content_license.content_metadata.chapter_info missing"] };
|
|
1314
|
+
}
|
|
1315
|
+
if (!("chapters" in data.content_license.content_metadata.chapter_info)) {
|
|
1316
|
+
return { errors: ["JSON Structure: key content_license.content_metadata.chapter_info.chapters missing"] };
|
|
1317
|
+
}
|
|
1318
|
+
if (!Array.isArray(data.content_license.content_metadata.chapter_info.chapters)) {
|
|
1319
|
+
return { errors: ["JSON Structure: content_license.content_metadata.chapter_info.chapters must be an array"] };
|
|
1320
|
+
}
|
|
1321
|
+
if (!data.content_license.content_metadata.chapter_info.chapters.every((chapter) => "start_offset_sec" in chapter)) {
|
|
1322
|
+
return { errors: ["JSON Structure: every chapter must have a start property"] };
|
|
1323
|
+
}
|
|
1324
|
+
return { errors: [] };
|
|
1325
|
+
}
|
|
1326
|
+
parse(string) {
|
|
1327
|
+
const data = JSON.parse(string);
|
|
1328
|
+
const { errors } = this.test(data);
|
|
1329
|
+
if (errors.length > 0) {
|
|
1330
|
+
throw new Error(errors.join(""));
|
|
1331
|
+
}
|
|
1332
|
+
this.duration = data.content_license.content_metadata.chapter_info.runtime_length_ms;
|
|
1333
|
+
this.chapters = data.content_license.content_metadata.chapter_info.chapters.map((raw) => {
|
|
1334
|
+
const { start_offset_ms: startTime, title } = raw;
|
|
1335
|
+
const chapter = {
|
|
1336
|
+
startTime
|
|
1337
|
+
};
|
|
1338
|
+
if (title) {
|
|
1339
|
+
chapter.title = title;
|
|
1340
|
+
}
|
|
1341
|
+
return chapter;
|
|
1342
|
+
});
|
|
1343
|
+
this.bump();
|
|
1344
|
+
}
|
|
1345
|
+
toString(pretty = false) {
|
|
1346
|
+
return JSON.stringify({
|
|
1347
|
+
content_license: {
|
|
1348
|
+
content_metadata: {
|
|
1349
|
+
chapter_info: {
|
|
1350
|
+
brandIntroDurationMs: 2043,
|
|
1351
|
+
brandOutroDurationMs: 5061,
|
|
1352
|
+
chapters: this.chapters.map((chapter, i) => ({
|
|
1353
|
+
length_ms: Math.round(chapter.duration),
|
|
1354
|
+
start_offset_ms: chapter.startTime,
|
|
1355
|
+
start_offset_sec: Math.round(chapter.startTime / 1e3),
|
|
1356
|
+
title: this.ensureTitle(i)
|
|
1357
|
+
}))
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
}, null, pretty ? 2 : 0);
|
|
1362
|
+
}
|
|
1363
|
+
};
|
|
1364
|
+
|
|
1365
|
+
// src/Formats/Podigee.ts
|
|
1366
|
+
var Podigee = class extends Base {
|
|
1367
|
+
supportsPrettyPrint = true;
|
|
1368
|
+
test(data) {
|
|
1369
|
+
if (!Array.isArray(data)) {
|
|
1370
|
+
return { errors: ["JSON Structure: must be an array"] };
|
|
1371
|
+
}
|
|
1372
|
+
if (data.length === 0) {
|
|
1373
|
+
return { errors: ["JSON Structure: must not be empty"] };
|
|
1374
|
+
}
|
|
1375
|
+
if (!data.every((chapter) => "start_time" in chapter && "title" in chapter)) {
|
|
1376
|
+
return { errors: ["JSON Structure: every chapter must have a start_time and title property"] };
|
|
1377
|
+
}
|
|
1378
|
+
return { errors: [] };
|
|
1379
|
+
}
|
|
1380
|
+
parse(string) {
|
|
1381
|
+
const data = JSON.parse(string);
|
|
1382
|
+
const { errors } = this.test(data);
|
|
1383
|
+
if (errors.length > 0) {
|
|
1384
|
+
throw new Error(errors.join(""));
|
|
1385
|
+
}
|
|
1386
|
+
this.chapters = data.map((raw) => {
|
|
1387
|
+
const { start_time: start, title, image, url } = raw;
|
|
1388
|
+
const chapter = {
|
|
1389
|
+
startTime: timestampToSeconds(start)
|
|
1390
|
+
};
|
|
1391
|
+
if (title) {
|
|
1392
|
+
chapter.title = title;
|
|
1393
|
+
}
|
|
1394
|
+
if (image) {
|
|
1395
|
+
chapter.img = image;
|
|
1396
|
+
}
|
|
1397
|
+
if (url) {
|
|
1398
|
+
chapter.url = url;
|
|
1399
|
+
}
|
|
1400
|
+
return chapter;
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
toString(pretty = false) {
|
|
1404
|
+
return JSON.stringify(this.chapters.map((chapter, i) => {
|
|
1405
|
+
const output = {
|
|
1406
|
+
start_time: secondsToTimestamp(chapter.startTime),
|
|
1407
|
+
title: this.ensureTitle(i)
|
|
1408
|
+
};
|
|
1409
|
+
if (chapter.img) {
|
|
1410
|
+
output.image = chapter.img;
|
|
1411
|
+
}
|
|
1412
|
+
if (chapter.url) {
|
|
1413
|
+
output.url = chapter.url;
|
|
1414
|
+
}
|
|
1415
|
+
return output;
|
|
1416
|
+
}), null, pretty ? 2 : 0);
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1266
1420
|
// src/Formats/AutoFormat.ts
|
|
1267
1421
|
var classMap = {
|
|
1268
1422
|
chaptersjson: ChaptersJson,
|
|
@@ -1281,7 +1435,9 @@ var classMap = {
|
|
|
1281
1435
|
mp4chaps: MP4Chaps,
|
|
1282
1436
|
podlovejson: PodloveJson,
|
|
1283
1437
|
applehls: AppleHLS,
|
|
1284
|
-
scenecut: Scenecut
|
|
1438
|
+
scenecut: Scenecut,
|
|
1439
|
+
audible: Audible,
|
|
1440
|
+
podigee: Podigee
|
|
1285
1441
|
};
|
|
1286
1442
|
var AutoFormat = {
|
|
1287
1443
|
classMap,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mtillmann/chapters",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "library that manages and converts chapters of multiple formats",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"podcast-chapters",
|
|
35
35
|
"edl",
|
|
36
36
|
"hls",
|
|
37
|
-
"scenecut"
|
|
37
|
+
"scenecut",
|
|
38
|
+
"podigee"
|
|
38
39
|
],
|
|
39
40
|
"license": "MIT",
|
|
40
41
|
"devDependencies": {
|
package/readme.md
CHANGED
|
@@ -24,6 +24,7 @@ This is the core library of the [chaptertool](https://github.com/Mtillmann/chapt
|
|
|
24
24
|
| VorbisComment | Vorbis Comment Format | vorbiscomment | `txt` | [spec](https://wiki.xiph.org/Chapter_Extension) |
|
|
25
25
|
| AppleChapters | "Apple Chapters" | applechapters | `xml` | [source](https://github.com/rigaya/NVEnc/blob/master/NVEncC_Options.en.md#--chapter-string:~:text=CHAPTER03NAME%3Dchapter%2D3-,apple%20format,-(should%20be%20in)) |
|
|
26
26
|
| ShutterEDL | Shutter EDL | edl | `edl` | [source](https://github.com/paulpacifico/shutter-encoder/blob/f3d6bb6dfcd629861a0b0a50113bf4b062e1ba17/src/application/SceneDetection.java) |
|
|
27
|
+
| Podigee | Podigee Chapters/Chaptermarks | podigee | `json` | [spec](https://app.podigee.com/api-docs#!/ChapterMarks/updateChapterMark:~:text=Model-,Example%20Value,-%7B%0A%20%20%22title%22%3A%20%22string%22%2C%0A%20%20%22start_time) |
|
|
27
28
|
| PodloveSimpleChapters | Podlove Simple Chapters | psc | `xml` | [spec](https://podlove.org/simple-chapters/) |
|
|
28
29
|
| PodloveJson | Podlove Simple Chapters JSON | podlovejson | `json` | [source](https://github.com/podlove/chapters#:~:text=org/%3E-,Encode%20to%20JSON,-iex%3E%20Chapters) |
|
|
29
30
|
| MP4Chaps | MP4Chaps | mp4chaps | `txt` | [source](https://github.com/podlove/chapters#:~:text=%3Achapters%3E-,Encode%20to%20mp4chaps,-iex%3E%20Chapters) |
|