@mtillmann/chapters 0.1.6 → 0.1.8

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 CHANGED
@@ -70,7 +70,7 @@ declare abstract class Base implements MediaItem {
70
70
  static create(input?: string | MediaItem, duration?: number): MediaItem;
71
71
  from(input?: string | MediaItem): MediaItem;
72
72
  detect(inputString: string): boolean;
73
- test(data: object): {
73
+ test(data: Record<string, any>): {
74
74
  errors: string[];
75
75
  };
76
76
  bump(keepDuration?: boolean): void;
@@ -82,8 +82,8 @@ declare abstract class Base implements MediaItem {
82
82
  parse(string: string): void;
83
83
  toString(pretty?: boolean, exportOptions?: {}): string;
84
84
  applyChapterMinLength(seconds: number): object;
85
- addChapterAt(index: number, chapter?: object): number;
86
- addChapterAtTime(time: number | string, chapter?: object): boolean;
85
+ addChapterAt(index: number, chapter?: Partial<Chapter>): number;
86
+ addChapterAtTime(time: number | string, chapter?: Partial<Chapter>): boolean;
87
87
  rebuildChapterTitles(template?: string): void;
88
88
  ensureTitle(index: number): string;
89
89
  getChapterTitle(index: number, template?: string): string;
@@ -129,6 +129,16 @@ declare class AppleHLS extends Base {
129
129
  toString(pretty?: boolean): string;
130
130
  }
131
131
 
132
+ declare class Audible extends Base {
133
+ filename: string;
134
+ mimeType: string;
135
+ test(data: Record<string, any>): {
136
+ errors: string[];
137
+ };
138
+ parse(string: string): void;
139
+ toString(pretty?: boolean): string;
140
+ }
141
+
132
142
  declare class ChaptersJson extends Base {
133
143
  supportsPrettyPrint: boolean;
134
144
  }
@@ -179,6 +189,39 @@ declare class MP4Chaps extends Base {
179
189
  toString(): string;
180
190
  }
181
191
 
192
+ declare class TextGeneric extends Base {
193
+ filename: string;
194
+ mimeType: string;
195
+ formats: Record<string, Record<string, any>>;
196
+ detect(inputString: string): boolean;
197
+ parse(string: string): void;
198
+ toString(pretty?: boolean, exportOptions?: string | Record<string, any>): string;
199
+ }
200
+
201
+ declare class PodcastPage extends TextGeneric {
202
+ filename: string;
203
+ mimeType: string;
204
+ detect(inputString: string): boolean;
205
+ toString(): string;
206
+ }
207
+
208
+ declare class Podigee extends Base {
209
+ filename: string;
210
+ supportsPrettyPrint: boolean;
211
+ test(data: object[]): {
212
+ errors: string[];
213
+ };
214
+ parse(string: string): void;
215
+ toString(pretty?: boolean): string;
216
+ }
217
+
218
+ declare class PodigeeText extends TextGeneric {
219
+ filename: string;
220
+ mimeType: string;
221
+ detect(inputString: string): boolean;
222
+ toString(): string;
223
+ }
224
+
182
225
  declare class PodloveJson extends Base {
183
226
  filename: string;
184
227
  mimeType: string;
@@ -224,6 +267,13 @@ declare class Scenecut extends Base {
224
267
  toString(pretty?: boolean, exportOptions?: {}): string;
225
268
  }
226
269
 
270
+ declare class ShowNotes extends TextGeneric {
271
+ filename: string;
272
+ mimeType: string;
273
+ detect(inputString: string): boolean;
274
+ toString(): string;
275
+ }
276
+
227
277
  declare class ShutterEDL extends Base {
228
278
  detect(inputString: string): boolean;
229
279
  decodeTime(timeString: string): string;
@@ -232,6 +282,27 @@ declare class ShutterEDL extends Base {
232
282
  toString(): string;
233
283
  }
234
284
 
285
+ declare class SpotifyA extends TextGeneric {
286
+ filename: string;
287
+ mimeType: string;
288
+ detect(inputString: string): boolean;
289
+ toString(): string;
290
+ }
291
+
292
+ declare class SpotifyB extends TextGeneric {
293
+ filename: string;
294
+ mimeType: string;
295
+ detect(inputString: string): boolean;
296
+ toString(): string;
297
+ }
298
+
299
+ declare class TransistorFM extends TextGeneric {
300
+ filename: string;
301
+ mimeType: string;
302
+ detect(inputString: string): boolean;
303
+ toString(): string;
304
+ }
305
+
235
306
  declare class VorbisComment extends MKVMergeSimple {
236
307
  filename: string;
237
308
  mimeType: string;
@@ -246,11 +317,10 @@ declare class WebVTT extends Base {
246
317
  toString(): string;
247
318
  }
248
319
 
249
- declare class Youtube extends Base {
320
+ declare class Youtube extends TextGeneric {
250
321
  filename: string;
251
322
  mimeType: string;
252
323
  detect(inputString: string): boolean;
253
- parse(string: string): void;
254
324
  toString(): string;
255
325
  }
256
326
 
@@ -294,4 +364,4 @@ declare namespace util {
294
364
  export { util_Float as Float, util_Floats as Floats, util_Int as Int, util_Ints as Ints, util_NPTToSeconds as NPTToSeconds, util_enforceMilliseconds as enforceMilliseconds, util_escapeRegExpCharacters as escapeRegExpCharacters, util_formatBytes as formatBytes, util_hash as hash, util_indenter as indenter, util_secondsToNPT as secondsToNPT, util_secondsToTimestamp as secondsToTimestamp, util_stringToLines as stringToLines, util_timestampToSeconds as timestampToSeconds, util_toSeconds as toSeconds, util_zeroPad as zeroPad };
295
365
  }
296
366
 
297
- export { AppleChapters, AppleHLS, AutoFormat, type Chapter, ChaptersJson, FFMetadata, FFMpegInfo, MKVMergeSimple, MKVMergeXML, MP4Chaps, MatroskaXML, type MediaItem, type MediaItemMeta, PodloveJson, PodloveSimpleChapters, PySceneDetect, Scenecut, ShutterEDL, util as Util, VorbisComment, WebVTT, Youtube };
367
+ export { AppleChapters, AppleHLS, Audible, AutoFormat, type Chapter, ChaptersJson, FFMetadata, FFMpegInfo, MKVMergeSimple, MKVMergeXML, MP4Chaps, MatroskaXML, type MediaItem, type MediaItemMeta, PodcastPage, Podigee, PodigeeText, PodloveJson, PodloveSimpleChapters, PySceneDetect, Scenecut, ShowNotes, ShutterEDL, SpotifyA, SpotifyB, TransistorFM, util as Util, VorbisComment, WebVTT, Youtube };
package/dist/index.js CHANGED
@@ -909,38 +909,88 @@ var WebVTT = class extends Base {
909
909
  }
910
910
  };
911
911
 
912
- // src/Formats/Youtube.ts
913
- var Youtube = class extends Base {
914
- filename = "youtube-chapters.txt";
912
+ // src/Formats/TextGeneric.ts
913
+ var TextGeneric = class extends Base {
914
+ filename = "chapters.txt";
915
915
  mimeType = "text/plain";
916
+ formats = {
917
+ spotifya: {
918
+ string: (title, start) => `(${start}) ${title}`,
919
+ hours: "default"
920
+ },
921
+ spotifyb: {
922
+ string: (title, start) => `${start}-${title}`,
923
+ hours: "default"
924
+ },
925
+ youtube: {
926
+ string: (title, start) => `${start} ${title}`,
927
+ hours: "whenOverOneHour",
928
+ enforceZeroStartTime: true
929
+ },
930
+ shownotes: {
931
+ string: (title, start) => [`(${start})`, "", title, ""].join("\n"),
932
+ hours: "default"
933
+ },
934
+ transistorfm: {
935
+ string: (title, start) => `${start} - ${title}`,
936
+ hours: "default"
937
+ },
938
+ podigeetext: {
939
+ string: (title, start) => `${start} - ${title}`,
940
+ hours: "always"
941
+ },
942
+ podcastpage: {
943
+ string: (title, start) => `(${start}) - ${title}`,
944
+ hours: "default"
945
+ }
946
+ };
916
947
  detect(inputString) {
917
- return /^0?0:00(:00)?\s/.test(inputString.trim());
948
+ return /^\(?(?<ts>\d?\d:\d\d(?::\d\d)?)\)?[\s-]+(?<title>[^\n]+)/.test(inputString.trim());
918
949
  }
919
950
  parse(string) {
920
951
  if (!this.detect(string)) {
921
- throw new Error("Youtube Chapters *MUST* begin with (0)0:00(:00), received: " + string.substr(0, 10) + "...");
952
+ throw new Error("Invalid format, see documentation for supported formats");
922
953
  }
923
- this.chapters = stringToLines(string).map((line) => {
924
- const l = line.split(" ");
925
- const timestamp = String(l.shift());
954
+ const matches = [...string.matchAll(/^\(?(?<ts>\d?\d:\d\d(?::\d\d)?)\)?[\s-]+(?<title>[^\n]+)/gm)];
955
+ this.chapters = matches.map((match) => {
926
956
  return {
927
- startTime: timestampToSeconds(timestamp),
928
- title: l.join(" ")
957
+ startTime: timestampToSeconds(match.groups.ts),
958
+ title: match.groups.title
929
959
  };
930
960
  });
931
961
  }
932
- toString() {
933
- const options = {
934
- milliseconds: false,
935
- hours: this.chapters.at(-1).startTime > 3600
936
- };
962
+ toString(pretty = false, exportOptions = "youtube") {
963
+ const formatKey = typeof exportOptions === "string" ? exportOptions : exportOptions.format;
964
+ if (!(formatKey in this.formats)) {
965
+ throw new Error("Invalid format: " + formatKey);
966
+ }
967
+ const template = this.formats[formatKey];
968
+ const exceedsOneHour = this.chapters.at(-1).startTime > 3600;
937
969
  return this.chapters.map((chapter, index) => {
938
- const startTime = index === 0 && chapter.startTime !== 0 ? 0 : chapter.startTime;
939
- return `${secondsToTimestamp(startTime, options)} ${this.ensureTitle(index)}`;
970
+ const startTime = template.enforceZeroStartTime && index === 0 && chapter.startTime !== 0 ? 0 : chapter.startTime;
971
+ let hours;
972
+ if (startTime > 3600 || template.hours === "always" || template.hours === "whenOverOneHour" && exceedsOneHour) {
973
+ hours = true;
974
+ } else {
975
+ hours = false;
976
+ }
977
+ return template.string(chapter.title, secondsToTimestamp(startTime, { hours }));
940
978
  }).join("\n");
941
979
  }
942
980
  };
943
981
 
982
+ // src/Formats/Youtube.ts
983
+ var Youtube = class extends TextGeneric {
984
+ filename = "youtube-chapters.txt";
985
+ mimeType = "text/plain";
986
+ detect(inputString) {
987
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) [^-](?<title>[^\n]+)/.test(inputString.trim());
988
+ }
989
+ toString() {
990
+ return super.toString(false, { format: "youtube" });
991
+ }
992
+ };
993
+
944
994
  // src/Formats/ShutterEDL.ts
945
995
  var ShutterEDL = class extends Base {
946
996
  // this format is based on the shutter encoder edl format
@@ -1362,6 +1412,134 @@ var Audible = class extends Base {
1362
1412
  }
1363
1413
  };
1364
1414
 
1415
+ // src/Formats/Podigee.ts
1416
+ var Podigee = class extends Base {
1417
+ filename = "podigee-chapters.json";
1418
+ supportsPrettyPrint = true;
1419
+ test(data) {
1420
+ if (!Array.isArray(data)) {
1421
+ return { errors: ["JSON Structure: must be an array"] };
1422
+ }
1423
+ if (data.length === 0) {
1424
+ return { errors: ["JSON Structure: must not be empty"] };
1425
+ }
1426
+ if (!data.every((chapter) => "start_time" in chapter && "title" in chapter)) {
1427
+ return { errors: ["JSON Structure: every chapter must have a start_time and title property"] };
1428
+ }
1429
+ return { errors: [] };
1430
+ }
1431
+ parse(string) {
1432
+ const data = JSON.parse(string);
1433
+ const { errors } = this.test(data);
1434
+ if (errors.length > 0) {
1435
+ throw new Error(errors.join(""));
1436
+ }
1437
+ this.chapters = data.map((raw) => {
1438
+ const { start_time: start, title, image, url } = raw;
1439
+ const chapter = {
1440
+ startTime: timestampToSeconds(start)
1441
+ };
1442
+ if (title) {
1443
+ chapter.title = title;
1444
+ }
1445
+ if (image) {
1446
+ chapter.img = image;
1447
+ }
1448
+ if (url) {
1449
+ chapter.url = url;
1450
+ }
1451
+ return chapter;
1452
+ });
1453
+ }
1454
+ toString(pretty = false) {
1455
+ return JSON.stringify(this.chapters.map((chapter, i) => {
1456
+ const output = {
1457
+ start_time: secondsToTimestamp(chapter.startTime),
1458
+ title: this.ensureTitle(i)
1459
+ };
1460
+ if (chapter.img) {
1461
+ output.image = chapter.img;
1462
+ }
1463
+ if (chapter.url) {
1464
+ output.url = chapter.url;
1465
+ }
1466
+ return output;
1467
+ }), null, pretty ? 2 : 0);
1468
+ }
1469
+ };
1470
+
1471
+ // src/Formats/PodigeeText.ts
1472
+ var PodigeeText = class extends TextGeneric {
1473
+ filename = "podigee-chapters.txt";
1474
+ mimeType = "text/plain";
1475
+ detect(inputString) {
1476
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) - (?<title>[^\n]+)/.test(inputString.trim());
1477
+ }
1478
+ toString() {
1479
+ return super.toString(false, { format: "podigeetext" });
1480
+ }
1481
+ };
1482
+
1483
+ // src/Formats/ShowNotes.ts
1484
+ var ShowNotes = class extends TextGeneric {
1485
+ filename = "shownote-chapters.txt";
1486
+ mimeType = "text/plain";
1487
+ detect(inputString) {
1488
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\)\n\n(?<title>[^\n]+)/.test(inputString.trim());
1489
+ }
1490
+ toString() {
1491
+ return super.toString(false, { format: "shownotes" });
1492
+ }
1493
+ };
1494
+
1495
+ // src/Formats/SpotifyA.ts
1496
+ var SpotifyA = class extends TextGeneric {
1497
+ filename = "spotify-chapters.txt";
1498
+ mimeType = "text/plain";
1499
+ detect(inputString) {
1500
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\) [^-](?<title>[^\n]+)/.test(inputString.trim());
1501
+ }
1502
+ toString() {
1503
+ return super.toString(false, { format: "spotifya" });
1504
+ }
1505
+ };
1506
+
1507
+ // src/Formats/SpotifyB.ts
1508
+ var SpotifyB = class extends TextGeneric {
1509
+ filename = "spotify-chapters.txt";
1510
+ mimeType = "text/plain";
1511
+ detect(inputString) {
1512
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?)-(?<title>[^\n]+)/.test(inputString.trim());
1513
+ }
1514
+ toString() {
1515
+ return super.toString(false, { format: "spotifyb" });
1516
+ }
1517
+ };
1518
+
1519
+ // src/Formats/PodcastPage.ts
1520
+ var PodcastPage = class extends TextGeneric {
1521
+ filename = "podcastpage-chapters.txt";
1522
+ mimeType = "text/plain";
1523
+ detect(inputString) {
1524
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\) - (?<title>[^\n]+)/.test(inputString.trim());
1525
+ }
1526
+ toString() {
1527
+ return super.toString(false, { format: "podcastpage" });
1528
+ }
1529
+ };
1530
+
1531
+ // src/Formats/TransistorFM.ts
1532
+ var TransistorFM = class extends TextGeneric {
1533
+ filename = "transistorfm-chapters.txt";
1534
+ mimeType = "text/plain";
1535
+ detect(inputString) {
1536
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) - (?<title>[^\n]+)/.test(inputString.trim());
1537
+ }
1538
+ toString() {
1539
+ return super.toString(false, { format: "transistorfm" });
1540
+ }
1541
+ };
1542
+
1365
1543
  // src/Formats/AutoFormat.ts
1366
1544
  var classMap = {
1367
1545
  chaptersjson: ChaptersJson,
@@ -1381,7 +1559,14 @@ var classMap = {
1381
1559
  podlovejson: PodloveJson,
1382
1560
  applehls: AppleHLS,
1383
1561
  scenecut: Scenecut,
1384
- audible: Audible
1562
+ audible: Audible,
1563
+ podigee: Podigee,
1564
+ podigeetext: PodigeeText,
1565
+ shownotes: ShowNotes,
1566
+ spotifya: SpotifyA,
1567
+ spotifyb: SpotifyB,
1568
+ podcastpage: PodcastPage,
1569
+ transistorfm: TransistorFM
1385
1570
  };
1386
1571
  var AutoFormat = {
1387
1572
  classMap,
@@ -1421,6 +1606,7 @@ var AutoFormat = {
1421
1606
  export {
1422
1607
  AppleChapters,
1423
1608
  AppleHLS,
1609
+ Audible,
1424
1610
  AutoFormat,
1425
1611
  ChaptersJson,
1426
1612
  FFMetadata,
@@ -1429,11 +1615,18 @@ export {
1429
1615
  MKVMergeXML,
1430
1616
  MP4Chaps,
1431
1617
  MatroskaXML,
1618
+ PodcastPage,
1619
+ Podigee,
1620
+ PodigeeText,
1432
1621
  PodloveJson,
1433
1622
  PodloveSimpleChapters,
1434
1623
  PySceneDetect,
1435
1624
  Scenecut,
1625
+ ShowNotes,
1436
1626
  ShutterEDL,
1627
+ SpotifyA,
1628
+ SpotifyB,
1629
+ TransistorFM,
1437
1630
  util_exports as Util,
1438
1631
  VorbisComment,
1439
1632
  WebVTT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtillmann/chapters",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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
@@ -4,7 +4,7 @@
4
4
 
5
5
  # chapters
6
6
 
7
- This is the core library of the [chaptertool](https://github.com/Mtillmann/chaptertool) project and provides the functionality to handle the formats below.
7
+ This is the core library of [chaptertool](https://github.com/Mtillmann/chaptertool) and [chapconv](https://github.com/Mtillmann/chapconv/) and provides the functionality to handle the formats below.
8
8
 
9
9
  [Click here to open the web GUI](https://mtillmann.github.io/chaptertool) or go install chaptertool locally for CLI use.
10
10
 
@@ -24,12 +24,18 @@ 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) |
30
31
  | AppleHLS | Apple HLS Chapters | applehls | `json` | [spec](https://developer.apple.com/documentation/http-live-streaming/providing-javascript-object-notation-json-chapters), partial support |
31
32
  | Scenecut | Scenecut format | scenecut | `json` | [source](https://github.com/slhck/scenecut-extractor#:~:text=cuts%20in%20JSON-,format,-%3A) |
32
33
  | Audible | Audible Chapter Format | audible | `json` | [source](./audible-chapter-spec.md) |
34
+ | Spotify | Spotify Formats A/B | spotifya\|spotifyb | `txt` | [see](misc-text-chapter-spec.md) |
35
+ | Podcastpage | Podcastpage Format | podcastpage | `txt` | [see](misc-text-chapter-spec.md) |
36
+ | Podigee Text | Podigee Text Format | podigeetext | `txt` | [see](misc-text-chapter-spec.md) |
37
+ | TransistorFM | TransistorFM Chapter Format | transistorfm | `txt` | [see](misc-text-chapter-spec.md) |
38
+ | "Show Notes Chapters" | Unknown Shownotes Format | shownotes | `txt` | [see](misc-text-chapter-spec.md) |
33
39
 
34
40
  ## Installation
35
41
 
@@ -85,7 +91,7 @@ const chapters = (new ChaptersJson(3600)).from(input)
85
91
  ```
86
92
 
87
93
  > the constructor will not accept any input due to javascript's order of initialization which prevents the parse method
88
- > from having access to cerain locally defined properties and methods.
94
+ > from having access to certain locally defined properties and methods.
89
95
 
90
96
  ### `static create (input?: string | MediaItem): MediaItem`
91
97
 
@@ -96,7 +102,7 @@ const chapters = MatroskaXML.create(input)
96
102
  // chapters is now an instance of MatroskaXML
97
103
 
98
104
  const chapterString = WebVTT.create(chapters).toString()
99
- // chapterString is now a string representation of the chapters
105
+ // chapterString is now a WebVTT string representation of the chapters
100
106
  ```
101
107
 
102
108
  ### `from (input?: string | MediaItem): MediaItem`
@@ -111,11 +117,11 @@ Converts the media item to another format.
111
117
 
112
118
  Adds a chapter.
113
119
 
114
- ### `addChapterAt (index: number, chapter: object = {}): number`
120
+ ### `addChapterAt (index: number, chapter: Partial<Chapter> = {}): number`
115
121
 
116
122
  Adds a chapter at the given index.
117
123
 
118
- ### `addChapterAtTime (time: number | string, chapter: object = {}): boolean`
124
+ ### `addChapterAtTime (time: number | string, chapter: Partial<Chapter> = {}): boolean`
119
125
 
120
126
  Adds a chapter at the given time.
121
127