Skip to content

Commit 96ab29f

Browse files
committed
feat: add Podigee format support with corresponding tests
1 parent 64ebfaf commit 96ab29f

File tree

7 files changed

+164
-4
lines changed

7 files changed

+164
-4
lines changed

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@mtillmann/chapters",
3-
"version": "0.1.6",
3+
"version": "0.1.7",
44
"description": "library that manages and converts chapters of multiple formats",
55
"main": "dist/index.js",
66
"types": "dist/index.d.ts",
@@ -34,7 +34,8 @@
3434
"podcast-chapters",
3535
"edl",
3636
"hls",
37-
"scenecut"
37+
"scenecut",
38+
"podigee"
3839
],
3940
"license": "MIT",
4041
"devDependencies": {

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ This is the core library of the [chaptertool](https://github.com/Mtillmann/chapt
2424
| VorbisComment | Vorbis Comment Format | vorbiscomment | `txt` | [spec](https://wiki.xiph.org/Chapter_Extension) |
2525
| 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)) |
2626
| 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) |
2728
| PodloveSimpleChapters | Podlove Simple Chapters | psc | `xml` | [spec](https://podlove.org/simple-chapters/) |
2829
| PodloveJson | Podlove Simple Chapters JSON | podlovejson | `json` | [source](https://github.com/podlove/chapters#:~:text=org/%3E-,Encode%20to%20JSON,-iex%3E%20Chapters) |
2930
| MP4Chaps | MP4Chaps | mp4chaps | `txt` | [source](https://github.com/podlove/chapters#:~:text=%3Achapters%3E-,Encode%20to%20mp4chaps,-iex%3E%20Chapters) |

src/Formats/AutoFormat.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { PodloveJson } from './PodloveJson'
1616
import { AppleHLS } from './AppleHLS'
1717
import { Scenecut } from './Scenecut'
1818
import { Audible } from './Audible'
19+
import { Podigee } from './Podigee'
1920
import { type MediaItem } from '../Types/MediaItem'
2021

2122
const classMap: Record<string, any> = {
@@ -36,7 +37,8 @@ const classMap: Record<string, any> = {
3637
podlovejson: PodloveJson,
3738
applehls: AppleHLS,
3839
scenecut: Scenecut,
39-
audible: Audible
40+
audible: Audible,
41+
podigee: Podigee
4042
}
4143

4244
export const AutoFormat = {

src/Formats/Podigee.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { type Chapter } from '../Types/Chapter'
2+
import { secondsToTimestamp, timestampToSeconds } from '../util'
3+
import { Base } from './Base'
4+
5+
export class Podigee extends Base {
6+
supportsPrettyPrint = true
7+
8+
test (data: object[]): { errors: string[] } {
9+
if (!Array.isArray(data)) {
10+
return { errors: ['JSON Structure: must be an array'] }
11+
}
12+
13+
if (data.length === 0) {
14+
return { errors: ['JSON Structure: must not be empty'] }
15+
}
16+
17+
if (!data.every(chapter => 'start_time' in chapter && 'title' in chapter)) {
18+
return { errors: ['JSON Structure: every chapter must have a start_time and title property'] }
19+
}
20+
21+
return { errors: [] }
22+
}
23+
24+
parse (string: string): void {
25+
const data = JSON.parse(string)
26+
const { errors } = this.test(data as object[])
27+
if (errors.length > 0) {
28+
throw new Error(errors.join(''))
29+
}
30+
31+
this.chapters = data.map((raw: Record<string, any>) => {
32+
const { start_time: start, title, image, url } = raw
33+
const chapter: Chapter = {
34+
startTime: timestampToSeconds(start as string)
35+
}
36+
if (title) {
37+
chapter.title = title
38+
}
39+
if (image) {
40+
chapter.img = image
41+
}
42+
if (url) {
43+
chapter.url = url
44+
}
45+
return chapter
46+
})
47+
}
48+
49+
toString (pretty = false): string {
50+
return JSON.stringify(this.chapters.map((chapter, i) => {
51+
const output: Record<string, any> = {
52+
start_time: secondsToTimestamp(chapter.startTime),
53+
title: this.ensureTitle(i)
54+
}
55+
if (chapter.img) {
56+
output.image = chapter.img
57+
}
58+
if (chapter.url) {
59+
output.url = chapter.url
60+
}
61+
return output
62+
}), null, pretty ? 2 : 0)
63+
}
64+
}

tests/conversions.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,15 @@ import { PodloveJson } from "../src/Formats/PodloveJson";
1515
import { AppleHLS } from "../src/Formats/AppleHLS";
1616
import { Scenecut } from "../src/Formats/Scenecut";
1717
import { Audible } from "../src/Formats/Audible";
18+
import { Podigee } from "../src/Formats/Podigee";
1819
import { readFileSync } from "fs";
1920
import { sep } from "path";
2021

2122
describe('conversions from one format to any other', () => {
2223
const formats = [
2324
AppleChapters, AppleHLS, Audible, ChaptersJson,
2425
FFMetadata, MatroskaXML, MKVMergeSimple, MKVMergeXML,
25-
MP4Chaps, PodloveJson, PodloveSimpleChapters, PySceneDetect,
26+
MP4Chaps, Podigee, PodloveJson, PodloveSimpleChapters, PySceneDetect,
2627
Scenecut, ShutterEDL, VorbisComment, WebVTT, Youtube
2728
];
2829

tests/format_podigee.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { readFileSync } from "fs";
2+
import { sep } from "path";
3+
import { PodloveJson } from "../src/Formats/PodloveJson";
4+
import { Podigee } from "../src/Formats/Podigee";
5+
6+
7+
describe('Podigee Format Handler', () => {
8+
it('accepts no arguments', () => {
9+
expect(() => {
10+
Podigee.create();
11+
}).not.toThrow(TypeError);
12+
});
13+
14+
15+
it('fails on malformed input', () => {
16+
expect(() => {
17+
Podigee.create('asdf');
18+
}).toThrow(Error);
19+
});
20+
21+
const content = readFileSync(module.path + sep + 'samples' + sep + 'podigee.json', 'utf-8');
22+
23+
it('parses well-formed input', () => {
24+
expect(() => {
25+
Podigee.create(content);
26+
}).not.toThrow(Error);
27+
});
28+
29+
const instance = Podigee.create(content);
30+
31+
it('has the correct number of chapters from content', () => {
32+
expect(instance.chapters.length).toEqual(5);
33+
});
34+
35+
it('has parsed the timestamps correctly', () => {
36+
expect(instance.chapters[1].startTime).toBe(112)
37+
});
38+
39+
it('has parsed the chapter titles correctly', () => {
40+
expect(instance.chapters[0].title).toBe('Einleitung')
41+
});
42+
43+
it('exports to correct format', () => {
44+
expect(instance.toString()).toContain('start_time":"');
45+
});
46+
47+
it('export includes correct timestamp', () => {
48+
expect(instance.toString()).toContain('00:01:52');
49+
});
50+
51+
it('can import previously generated export', () => {
52+
expect(Podigee.create(instance.toString()).chapters[2].startTime).toEqual(316);
53+
});
54+
55+
it('can convert into other format', () => {
56+
expect(instance.to(PodloveJson)).toBeInstanceOf(PodloveJson)
57+
});
58+
59+
it('will apply the pretty print option', () => {
60+
expect(instance.toString(true)).not.toEqual(instance.toString());
61+
});
62+
63+
it('fails on empty input array', () => {
64+
expect(() => {
65+
(new Podigee).from(JSON.stringify([]));
66+
}).toThrow(Error);
67+
});
68+
});

tests/samples/podigee.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"title": "Einleitung",
4+
"start_time": "00:00:00"
5+
},
6+
{
7+
"title": "Diskettenlyrik",
8+
"start_time": "00:01:52"
9+
},
10+
{
11+
"title": "ENTWICKLUNG DER DISKETTE",
12+
"start_time": "00:05:16"
13+
},
14+
{
15+
"title": "Erstes Speichermedium: Die Lochkarte",
16+
"start_time": "00:07:09"
17+
},
18+
{
19+
"title": "Das Magnetband",
20+
"start_time": "00:08:47"
21+
}
22+
23+
]

0 commit comments

Comments
 (0)