@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.
Files changed (3) hide show
  1. package/dist/index.js +210 -54
  2. package/package.json +3 -2
  3. 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 { JSDOM } from "jsdom";
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
- dom = new JSDOM(string, { contentType: "application/xml" });
515
- dom = dom.window.document;
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
- dom = new JSDOM2(string, { contentType: "application/xml" });
569
- dom = dom.window.document;
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 { JSDOM as JSDOM3 } from "jsdom";
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
- dom = new JSDOM3(string, { contentType: "application/xml" });
1003
- dom = dom.window.document;
1004
- }
1005
- this.chapters = [...dom.querySelectorAll("[start]")].reduce((acc, node) => {
1006
- if (node.tagName === "psc:chapter") {
1007
- const start = node.getAttribute("start");
1008
- const title = node.getAttribute("title");
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 (title) {
1015
- chapter.title = title;
1046
+ if (chapter["@_title"]) {
1047
+ item.title = chapter["@_title"];
1016
1048
  }
1017
- if (image) {
1018
- chapter.img = image;
1049
+ if (chapter["@_image"]) {
1050
+ item.img = chapter["@_image"];
1019
1051
  }
1020
- if (href) {
1021
- chapter.url = href;
1052
+ if (chapter["@_href"]) {
1053
+ item.url = chapter["@_href"];
1022
1054
  }
1023
- acc.push(chapter);
1024
- }
1025
- return acc;
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, '<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">')
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(3, node.join("")));
1086
+ output.push(indent(4, node.join("")));
1053
1087
  });
1054
1088
  output.push(
1055
- indent(2, "</psc:chapters>"),
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.5",
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) |