@mtillmann/chapters 0.1.7 → 0.2.0

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
@@ -8,9 +8,14 @@ interface Chapter {
8
8
  duration?: number;
9
9
  title?: string;
10
10
  url?: string;
11
- location?: string;
11
+ location?: ChapterLocation;
12
12
  toc?: boolean;
13
13
  }
14
+ interface ChapterLocation {
15
+ name: string;
16
+ geo: string;
17
+ osm?: string;
18
+ }
14
19
 
15
20
  interface MediaItemMeta {
16
21
  author?: string;
@@ -70,7 +75,7 @@ declare abstract class Base implements MediaItem {
70
75
  static create(input?: string | MediaItem, duration?: number): MediaItem;
71
76
  from(input?: string | MediaItem): MediaItem;
72
77
  detect(inputString: string): boolean;
73
- test(data: object): {
78
+ test(data: Record<string, any>): {
74
79
  errors: string[];
75
80
  };
76
81
  bump(keepDuration?: boolean): void;
@@ -82,8 +87,8 @@ declare abstract class Base implements MediaItem {
82
87
  parse(string: string): void;
83
88
  toString(pretty?: boolean, exportOptions?: {}): string;
84
89
  applyChapterMinLength(seconds: number): object;
85
- addChapterAt(index: number, chapter?: object): number;
86
- addChapterAtTime(time: number | string, chapter?: object): boolean;
90
+ addChapterAt(index: number, chapter?: Partial<Chapter>): number;
91
+ addChapterAtTime(time: number | string, chapter?: Partial<Chapter>): boolean;
87
92
  rebuildChapterTitles(template?: string): void;
88
93
  ensureTitle(index: number): string;
89
94
  getChapterTitle(index: number, template?: string): string;
@@ -129,6 +134,16 @@ declare class AppleHLS extends Base {
129
134
  toString(pretty?: boolean): string;
130
135
  }
131
136
 
137
+ declare class Audible extends Base {
138
+ filename: string;
139
+ mimeType: string;
140
+ test(data: Record<string, any>): {
141
+ errors: string[];
142
+ };
143
+ parse(string: string): void;
144
+ toString(pretty?: boolean): string;
145
+ }
146
+
132
147
  declare class ChaptersJson extends Base {
133
148
  supportsPrettyPrint: boolean;
134
149
  }
@@ -179,6 +194,39 @@ declare class MP4Chaps extends Base {
179
194
  toString(): string;
180
195
  }
181
196
 
197
+ declare class TextGeneric extends Base {
198
+ filename: string;
199
+ mimeType: string;
200
+ formats: Record<string, Record<string, any>>;
201
+ detect(inputString: string): boolean;
202
+ parse(string: string): void;
203
+ toString(pretty?: boolean, exportOptions?: string | Record<string, any>): string;
204
+ }
205
+
206
+ declare class PodcastPage extends TextGeneric {
207
+ filename: string;
208
+ mimeType: string;
209
+ detect(inputString: string): boolean;
210
+ toString(): string;
211
+ }
212
+
213
+ declare class Podigee extends Base {
214
+ filename: string;
215
+ supportsPrettyPrint: boolean;
216
+ test(data: object[]): {
217
+ errors: string[];
218
+ };
219
+ parse(string: string): void;
220
+ toString(pretty?: boolean): string;
221
+ }
222
+
223
+ declare class PodigeeText extends TextGeneric {
224
+ filename: string;
225
+ mimeType: string;
226
+ detect(inputString: string): boolean;
227
+ toString(): string;
228
+ }
229
+
182
230
  declare class PodloveJson extends Base {
183
231
  filename: string;
184
232
  mimeType: string;
@@ -224,6 +272,13 @@ declare class Scenecut extends Base {
224
272
  toString(pretty?: boolean, exportOptions?: {}): string;
225
273
  }
226
274
 
275
+ declare class ShowNotes extends TextGeneric {
276
+ filename: string;
277
+ mimeType: string;
278
+ detect(inputString: string): boolean;
279
+ toString(): string;
280
+ }
281
+
227
282
  declare class ShutterEDL extends Base {
228
283
  detect(inputString: string): boolean;
229
284
  decodeTime(timeString: string): string;
@@ -232,6 +287,27 @@ declare class ShutterEDL extends Base {
232
287
  toString(): string;
233
288
  }
234
289
 
290
+ declare class SpotifyA extends TextGeneric {
291
+ filename: string;
292
+ mimeType: string;
293
+ detect(inputString: string): boolean;
294
+ toString(): string;
295
+ }
296
+
297
+ declare class SpotifyB extends TextGeneric {
298
+ filename: string;
299
+ mimeType: string;
300
+ detect(inputString: string): boolean;
301
+ toString(): string;
302
+ }
303
+
304
+ declare class TransistorFM extends TextGeneric {
305
+ filename: string;
306
+ mimeType: string;
307
+ detect(inputString: string): boolean;
308
+ toString(): string;
309
+ }
310
+
235
311
  declare class VorbisComment extends MKVMergeSimple {
236
312
  filename: string;
237
313
  mimeType: string;
@@ -246,11 +322,10 @@ declare class WebVTT extends Base {
246
322
  toString(): string;
247
323
  }
248
324
 
249
- declare class Youtube extends Base {
325
+ declare class Youtube extends TextGeneric {
250
326
  filename: string;
251
327
  mimeType: string;
252
328
  detect(inputString: string): boolean;
253
- parse(string: string): void;
254
329
  toString(): string;
255
330
  }
256
331
 
@@ -294,4 +369,4 @@ declare namespace util {
294
369
  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
370
  }
296
371
 
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 };
372
+ 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
@@ -243,6 +243,14 @@ var Base = class {
243
243
  const endTime = this.endTime(index);
244
244
  const duration = endTime - this.chapters[index].startTime;
245
245
  const timestampOptions = { hours: false };
246
+ const location = {
247
+ ...{
248
+ name: "",
249
+ geo: "",
250
+ osm: ""
251
+ },
252
+ ..."location" in chapter ? chapter.location : {}
253
+ };
246
254
  return {
247
255
  ...{
248
256
  id: hash(),
@@ -250,6 +258,7 @@ var Base = class {
250
258
  },
251
259
  ...chapter,
252
260
  ...{
261
+ location,
253
262
  endTime,
254
263
  duration,
255
264
  startTime_hr: secondsToTimestamp(chapter.startTime, timestampOptions),
@@ -332,12 +341,24 @@ var Base = class {
332
341
  if (!("toc" in filtered) && options.writeRedundantToc) {
333
342
  filtered.toc = true;
334
343
  }
335
- ["location", "img", "url", "title"].forEach((property) => {
344
+ ["img", "url", "title"].forEach((property) => {
336
345
  const key = property;
337
346
  if (key in chapter && String(chapter[key]).trim().length > 0) {
338
347
  filtered[key] = chapter[key];
339
348
  }
340
349
  });
350
+ if ("location" in chapter) {
351
+ const name = chapter.location?.name?.trim();
352
+ const geo = chapter.location?.geo?.trim();
353
+ const osm = chapter.location?.osm?.trim();
354
+ if (name || geo || osm) {
355
+ filtered.location = {
356
+ name,
357
+ geo,
358
+ osm
359
+ };
360
+ }
361
+ }
341
362
  if ("img_filename" in chapter && "img" in filtered && chapter.img_type === "relative") {
342
363
  filtered.img = filenamify(chapter.img_filename);
343
364
  }
@@ -909,38 +930,88 @@ var WebVTT = class extends Base {
909
930
  }
910
931
  };
911
932
 
912
- // src/Formats/Youtube.ts
913
- var Youtube = class extends Base {
914
- filename = "youtube-chapters.txt";
933
+ // src/Formats/TextGeneric.ts
934
+ var TextGeneric = class extends Base {
935
+ filename = "chapters.txt";
915
936
  mimeType = "text/plain";
937
+ formats = {
938
+ spotifya: {
939
+ string: (title, start) => `(${start}) ${title}`,
940
+ hours: "default"
941
+ },
942
+ spotifyb: {
943
+ string: (title, start) => `${start}-${title}`,
944
+ hours: "default"
945
+ },
946
+ youtube: {
947
+ string: (title, start) => `${start} ${title}`,
948
+ hours: "whenOverOneHour",
949
+ enforceZeroStartTime: true
950
+ },
951
+ shownotes: {
952
+ string: (title, start) => [`(${start})`, "", title, ""].join("\n"),
953
+ hours: "default"
954
+ },
955
+ transistorfm: {
956
+ string: (title, start) => `${start} - ${title}`,
957
+ hours: "default"
958
+ },
959
+ podigeetext: {
960
+ string: (title, start) => `${start} - ${title}`,
961
+ hours: "always"
962
+ },
963
+ podcastpage: {
964
+ string: (title, start) => `(${start}) - ${title}`,
965
+ hours: "default"
966
+ }
967
+ };
916
968
  detect(inputString) {
917
- return /^0?0:00(:00)?\s/.test(inputString.trim());
969
+ return /^\(?(?<ts>\d?\d:\d\d(?::\d\d)?)\)?[\s-]+(?<title>[^\n]+)/.test(inputString.trim());
918
970
  }
919
971
  parse(string) {
920
972
  if (!this.detect(string)) {
921
- throw new Error("Youtube Chapters *MUST* begin with (0)0:00(:00), received: " + string.substr(0, 10) + "...");
973
+ throw new Error("Invalid format, see documentation for supported formats");
922
974
  }
923
- this.chapters = stringToLines(string).map((line) => {
924
- const l = line.split(" ");
925
- const timestamp = String(l.shift());
975
+ const matches = [...string.matchAll(/^\(?(?<ts>\d?\d:\d\d(?::\d\d)?)\)?[\s-]+(?<title>[^\n]+)/gm)];
976
+ this.chapters = matches.map((match) => {
926
977
  return {
927
- startTime: timestampToSeconds(timestamp),
928
- title: l.join(" ")
978
+ startTime: timestampToSeconds(match.groups.ts),
979
+ title: match.groups.title
929
980
  };
930
981
  });
931
982
  }
932
- toString() {
933
- const options = {
934
- milliseconds: false,
935
- hours: this.chapters.at(-1).startTime > 3600
936
- };
983
+ toString(pretty = false, exportOptions = "youtube") {
984
+ const formatKey = typeof exportOptions === "string" ? exportOptions : exportOptions.format;
985
+ if (!(formatKey in this.formats)) {
986
+ throw new Error("Invalid format: " + formatKey);
987
+ }
988
+ const template = this.formats[formatKey];
989
+ const exceedsOneHour = this.chapters.at(-1).startTime > 3600;
937
990
  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)}`;
991
+ const startTime = template.enforceZeroStartTime && index === 0 && chapter.startTime !== 0 ? 0 : chapter.startTime;
992
+ let hours;
993
+ if (startTime > 3600 || template.hours === "always" || template.hours === "whenOverOneHour" && exceedsOneHour) {
994
+ hours = true;
995
+ } else {
996
+ hours = false;
997
+ }
998
+ return template.string(chapter.title, secondsToTimestamp(startTime, { hours }));
940
999
  }).join("\n");
941
1000
  }
942
1001
  };
943
1002
 
1003
+ // src/Formats/Youtube.ts
1004
+ var Youtube = class extends TextGeneric {
1005
+ filename = "youtube-chapters.txt";
1006
+ mimeType = "text/plain";
1007
+ detect(inputString) {
1008
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) [^-](?<title>[^\n]+)/.test(inputString.trim());
1009
+ }
1010
+ toString() {
1011
+ return super.toString(false, { format: "youtube" });
1012
+ }
1013
+ };
1014
+
944
1015
  // src/Formats/ShutterEDL.ts
945
1016
  var ShutterEDL = class extends Base {
946
1017
  // this format is based on the shutter encoder edl format
@@ -1364,6 +1435,7 @@ var Audible = class extends Base {
1364
1435
 
1365
1436
  // src/Formats/Podigee.ts
1366
1437
  var Podigee = class extends Base {
1438
+ filename = "podigee-chapters.json";
1367
1439
  supportsPrettyPrint = true;
1368
1440
  test(data) {
1369
1441
  if (!Array.isArray(data)) {
@@ -1417,6 +1489,78 @@ var Podigee = class extends Base {
1417
1489
  }
1418
1490
  };
1419
1491
 
1492
+ // src/Formats/PodigeeText.ts
1493
+ var PodigeeText = class extends TextGeneric {
1494
+ filename = "podigee-chapters.txt";
1495
+ mimeType = "text/plain";
1496
+ detect(inputString) {
1497
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) - (?<title>[^\n]+)/.test(inputString.trim());
1498
+ }
1499
+ toString() {
1500
+ return super.toString(false, { format: "podigeetext" });
1501
+ }
1502
+ };
1503
+
1504
+ // src/Formats/ShowNotes.ts
1505
+ var ShowNotes = class extends TextGeneric {
1506
+ filename = "shownote-chapters.txt";
1507
+ mimeType = "text/plain";
1508
+ detect(inputString) {
1509
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\)\n\n(?<title>[^\n]+)/.test(inputString.trim());
1510
+ }
1511
+ toString() {
1512
+ return super.toString(false, { format: "shownotes" });
1513
+ }
1514
+ };
1515
+
1516
+ // src/Formats/SpotifyA.ts
1517
+ var SpotifyA = class extends TextGeneric {
1518
+ filename = "spotify-chapters.txt";
1519
+ mimeType = "text/plain";
1520
+ detect(inputString) {
1521
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\) [^-](?<title>[^\n]+)/.test(inputString.trim());
1522
+ }
1523
+ toString() {
1524
+ return super.toString(false, { format: "spotifya" });
1525
+ }
1526
+ };
1527
+
1528
+ // src/Formats/SpotifyB.ts
1529
+ var SpotifyB = class extends TextGeneric {
1530
+ filename = "spotify-chapters.txt";
1531
+ mimeType = "text/plain";
1532
+ detect(inputString) {
1533
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?)-(?<title>[^\n]+)/.test(inputString.trim());
1534
+ }
1535
+ toString() {
1536
+ return super.toString(false, { format: "spotifyb" });
1537
+ }
1538
+ };
1539
+
1540
+ // src/Formats/PodcastPage.ts
1541
+ var PodcastPage = class extends TextGeneric {
1542
+ filename = "podcastpage-chapters.txt";
1543
+ mimeType = "text/plain";
1544
+ detect(inputString) {
1545
+ return /^\((?<ts>\d?\d:\d\d(?::\d\d)?)\) - (?<title>[^\n]+)/.test(inputString.trim());
1546
+ }
1547
+ toString() {
1548
+ return super.toString(false, { format: "podcastpage" });
1549
+ }
1550
+ };
1551
+
1552
+ // src/Formats/TransistorFM.ts
1553
+ var TransistorFM = class extends TextGeneric {
1554
+ filename = "transistorfm-chapters.txt";
1555
+ mimeType = "text/plain";
1556
+ detect(inputString) {
1557
+ return /^(?<ts>\d?\d:\d\d(?::\d\d)?) - (?<title>[^\n]+)/.test(inputString.trim());
1558
+ }
1559
+ toString() {
1560
+ return super.toString(false, { format: "transistorfm" });
1561
+ }
1562
+ };
1563
+
1420
1564
  // src/Formats/AutoFormat.ts
1421
1565
  var classMap = {
1422
1566
  chaptersjson: ChaptersJson,
@@ -1437,7 +1581,13 @@ var classMap = {
1437
1581
  applehls: AppleHLS,
1438
1582
  scenecut: Scenecut,
1439
1583
  audible: Audible,
1440
- podigee: Podigee
1584
+ podigee: Podigee,
1585
+ podigeetext: PodigeeText,
1586
+ shownotes: ShowNotes,
1587
+ spotifya: SpotifyA,
1588
+ spotifyb: SpotifyB,
1589
+ podcastpage: PodcastPage,
1590
+ transistorfm: TransistorFM
1441
1591
  };
1442
1592
  var AutoFormat = {
1443
1593
  classMap,
@@ -1477,6 +1627,7 @@ var AutoFormat = {
1477
1627
  export {
1478
1628
  AppleChapters,
1479
1629
  AppleHLS,
1630
+ Audible,
1480
1631
  AutoFormat,
1481
1632
  ChaptersJson,
1482
1633
  FFMetadata,
@@ -1485,11 +1636,18 @@ export {
1485
1636
  MKVMergeXML,
1486
1637
  MP4Chaps,
1487
1638
  MatroskaXML,
1639
+ PodcastPage,
1640
+ Podigee,
1641
+ PodigeeText,
1488
1642
  PodloveJson,
1489
1643
  PodloveSimpleChapters,
1490
1644
  PySceneDetect,
1491
1645
  Scenecut,
1646
+ ShowNotes,
1492
1647
  ShutterEDL,
1648
+ SpotifyA,
1649
+ SpotifyB,
1650
+ TransistorFM,
1493
1651
  util_exports as Util,
1494
1652
  VorbisComment,
1495
1653
  WebVTT,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtillmann/chapters",
3
- "version": "0.1.7",
3
+ "version": "0.2.0",
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",
package/readme.md CHANGED
@@ -4,9 +4,9 @@
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
- [Click here to open the web GUI](https://mtillmann.github.io/chaptertool) or go install chaptertool locally for CLI use.
9
+ [Click here to open the chaptertool web GUI](https://mtillmann.github.io/chaptertool).
10
10
 
11
11
  ## Supported formats
12
12
 
@@ -31,6 +31,11 @@ This is the core library of the [chaptertool](https://github.com/Mtillmann/chapt
31
31
  | AppleHLS | Apple HLS Chapters | applehls | `json` | [spec](https://developer.apple.com/documentation/http-live-streaming/providing-javascript-object-notation-json-chapters), partial support |
32
32
  | Scenecut | Scenecut format | scenecut | `json` | [source](https://github.com/slhck/scenecut-extractor#:~:text=cuts%20in%20JSON-,format,-%3A) |
33
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) |
34
39
 
35
40
  ## Installation
36
41
 
@@ -86,7 +91,7 @@ const chapters = (new ChaptersJson(3600)).from(input)
86
91
  ```
87
92
 
88
93
  > the constructor will not accept any input due to javascript's order of initialization which prevents the parse method
89
- > from having access to cerain locally defined properties and methods.
94
+ > from having access to certain locally defined properties and methods.
90
95
 
91
96
  ### `static create (input?: string | MediaItem): MediaItem`
92
97
 
@@ -97,7 +102,7 @@ const chapters = MatroskaXML.create(input)
97
102
  // chapters is now an instance of MatroskaXML
98
103
 
99
104
  const chapterString = WebVTT.create(chapters).toString()
100
- // chapterString is now a string representation of the chapters
105
+ // chapterString is now a WebVTT string representation of the chapters
101
106
  ```
102
107
 
103
108
  ### `from (input?: string | MediaItem): MediaItem`
@@ -112,11 +117,11 @@ Converts the media item to another format.
112
117
 
113
118
  Adds a chapter.
114
119
 
115
- ### `addChapterAt (index: number, chapter: object = {}): number`
120
+ ### `addChapterAt (index: number, chapter: Partial<Chapter> = {}): number`
116
121
 
117
122
  Adds a chapter at the given index.
118
123
 
119
- ### `addChapterAtTime (time: number | string, chapter: object = {}): boolean`
124
+ ### `addChapterAtTime (time: number | string, chapter: Partial<Chapter> = {}): boolean`
120
125
 
121
126
  Adds a chapter at the given time.
122
127