Skip to content

Commit 052d435

Browse files
authored
Merge pull request #8 from Mtillmann/psc-etc
Podlove simple Chapters
2 parents 6d94e57 + f3ed377 commit 052d435

8 files changed

+254
-6
lines changed

src/Formats/AutoFormat.js

+4-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { VorbisComment } from "./VorbisComment.js";
1010
import { WebVTT } from "./WebVTT.js";
1111
import { Youtube } from "./Youtube.js";
1212
import { ShutterEDL } from "./ShutterEDL.js";
13+
import { PodloveSimpleChapters } from "./PodloveSimpleChapters.js";
1314

1415
export const AutoFormat = {
1516
classMap: {
@@ -24,7 +25,8 @@ export const AutoFormat = {
2425
pyscenedetect: PySceneDetect,
2526
vorbiscomment: VorbisComment,
2627
applechapters: AppleChapters,
27-
shutteredl: ShutterEDL
28+
shutteredl: ShutterEDL,
29+
psc: PodloveSimpleChapters
2830
},
2931

3032
detect(inputString, returnWhat = 'instance') {
@@ -45,6 +47,7 @@ export const AutoFormat = {
4547
}
4648
}
4749
} catch (e) {
50+
//console.log(e);
4851
//do nothing
4952
}
5053
});

src/Formats/PodloveSimpleChapters.js

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { FormatBase } from "./FormatBase.js";
2+
import jsdom from "jsdom";
3+
import { NPTToSeconds, secondsToNPT } from "../util.js";
4+
5+
export class PodloveSimpleChapters extends FormatBase {
6+
7+
supportsPrettyPrint = true;
8+
filename = 'podlove-simple-chapters-fragment.xml';
9+
mimeType = 'text/xml';
10+
11+
detect(inputString) {
12+
13+
return /<psc:chapters/.test(inputString);
14+
}
15+
16+
parse(string) {
17+
if (!this.detect(string)) {
18+
throw new Error('Input must contain <psc:chapters ...> node');
19+
}
20+
21+
let dom;
22+
if (typeof DOMParser !== 'undefined') {
23+
dom = (new DOMParser()).parseFromString(string, 'application/xml');
24+
} else {
25+
const { JSDOM } = jsdom;
26+
dom = new JSDOM(string, { contentType: 'application/xml' });
27+
dom = dom.window.document;
28+
}
29+
30+
31+
this.chapters = [...dom.querySelectorAll('[start]')].reduce((acc, node) => {
32+
33+
if (node.tagName === 'psc:chapter') {
34+
const start = node.getAttribute('start');
35+
const title = node.getAttribute('title');
36+
const image = node.getAttribute('image');
37+
const href = node.getAttribute('href');
38+
39+
const chapter = {
40+
startTime: NPTToSeconds(start)
41+
}
42+
43+
if (title) {
44+
chapter.title = title;
45+
}
46+
if (image) {
47+
chapter.img = image;
48+
}
49+
if (href) {
50+
//is this ever used, except for this format?
51+
chapter.href = href;
52+
}
53+
54+
acc.push(chapter);
55+
}
56+
return acc;
57+
58+
}, []);
59+
60+
}
61+
62+
toString(pretty = false) {
63+
const indent = (depth, string, spacesPerDepth = 2) => (pretty ? ' '.repeat(depth * spacesPerDepth) : '') + string;
64+
65+
let output = [
66+
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
67+
indent(1, '<channel>'),
68+
indent(2, '<!-- this is only a fragment of an rss feed, see -->'),
69+
indent(2, '<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->'),
70+
indent(2, '<!-- for more information -->'),
71+
indent(2, '<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">'),
72+
];
73+
74+
this.chapters.forEach(chapter => {
75+
76+
const node = [
77+
`<psc:chapter start="${secondsToNPT(chapter.startTime)}"`,
78+
];
79+
80+
if (chapter.title) {
81+
node.push(` title="${chapter.title}"`);
82+
}
83+
if (chapter.img) {
84+
node.push(` image="${chapter.img}"`);
85+
}
86+
if (chapter.href) {
87+
node.push(` href="${chapter.href}"`);
88+
}
89+
node.push('/>');
90+
91+
output.push(indent(3, node.join('')));
92+
93+
});
94+
95+
output.push(
96+
indent(2, '</psc:chapters>'),
97+
indent(1, '</channel>'),
98+
indent(0, '</rss>')
99+
);
100+
101+
return output.join(pretty ? "\n" : '');
102+
}
103+
}

src/Formats/ShutterEDL.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ export class ShutterEDL extends FormatBase {
4343
return acc;
4444
}
4545

46-
console.log(startTime, endTime, title);
47-
4846
acc.push({
4947
startTime,
5048
endTime,
@@ -56,7 +54,7 @@ export class ShutterEDL extends FormatBase {
5654

5755
toString() {
5856
// this format is weird, it expects 3 tracks per chapter, i suspect it's
59-
// V = video, A, A2 = stereo audio
57+
// V = video, [A, A2] = stereo audio
6058
const tracks = ['V', 'A', 'A2'];
6159
const output = this.chapters.reduce((acc, chapter,i) => {
6260

src/util.js

+49-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export function zeroPad(num, len = 3) {
33
}
44

55
export function secondsToTimestamp(s, options = {}) {
6-
options = {...{hours: true, milliseconds: false}, ...options};
6+
options = { ...{ hours: true, milliseconds: false }, ...options };
77

88
const date = new Date(parseInt(s) * 1000).toISOString();
99

@@ -22,6 +22,54 @@ export function secondsToTimestamp(s, options = {}) {
2222
return hms;
2323
}
2424

25+
/**
26+
* Converts a NPT (normal play time) to seconds, used by podlove simple chapters
27+
*/
28+
export function NPTToSeconds(npt) {
29+
let [parts, ms] = npt.split('.');
30+
ms = parseInt(ms || 0);
31+
parts = parts.split(':');
32+
33+
while (parts.length < 3) {
34+
parts.unshift(0);
35+
}
36+
37+
let [hours, minutes, seconds] = parts.map(i => parseInt(i));
38+
39+
return timestampToSeconds(`${zeroPad(hours, 2)}:${zeroPad(minutes, 2)}:${zeroPad(seconds, 2)}.${zeroPad(ms, 3)}`);
40+
}
41+
42+
export function secondsToNPT(seconds) {
43+
44+
if (seconds === 0) {
45+
return '0';
46+
}
47+
48+
const regularTimestamp = secondsToTimestamp(seconds, { milliseconds: true });
49+
let [hoursAndMinutesAndSeconds, milliseconds] = regularTimestamp.split('.');
50+
let [hours, minutes, secondsOnly] = hoursAndMinutesAndSeconds.split(':').map(i => parseInt(i));
51+
52+
if (milliseconds === '000') {
53+
milliseconds = '';
54+
} else {
55+
milliseconds = '.' + milliseconds;
56+
}
57+
58+
if (hours === 0 && minutes === 0) {
59+
return `${secondsOnly}${milliseconds}`;
60+
}
61+
62+
secondsOnly = zeroPad(secondsOnly, 2);
63+
64+
if(hours === 0){
65+
return `${minutes}:${secondsOnly}${milliseconds}`;
66+
}
67+
68+
minutes = zeroPad(minutes, 2);
69+
70+
return `${hours}:${minutes}:${secondsOnly}${milliseconds}`;
71+
}
72+
2573
export function timestampToSeconds(timestamp, fixedString = false) {
2674
let [seconds, minutes, hours] = timestamp.split(':').reverse();
2775
let milliseconds = 0;

tests/conversions.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
99
import { AppleChapters } from "../src/Formats/AppleChapters.js";
1010
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
1111
import { VorbisComment } from "../src/Formats/VorbisComment.js";
12+
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
1213
import { readFileSync } from "fs";
1314
import { sep } from "path";
1415

1516
describe('conversions from one format to any other', () => {
16-
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple, PySceneDetect, AppleChapters, ShutterEDL, VorbisComment];
17+
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple, PySceneDetect, AppleChapters, ShutterEDL, VorbisComment, PodloveSimpleChapters];
1718

1819
const content = readFileSync(module.path + sep + 'samples' + sep + 'chapters.json', 'utf-8');
1920

tests/format_psc.test.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
2+
import { readFileSync } from "fs";
3+
import { sep } from "path";
4+
import { FFMetadata } from "../src/Formats/FFMetadata.js";
5+
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
6+
7+
8+
describe('PodloveSimpleChapters Format Handler', () => {
9+
it('accepts no arguments', () => {
10+
expect(() => {
11+
new PodloveSimpleChapters();
12+
}).not.toThrowError(TypeError);
13+
});
14+
15+
16+
it('fails on malformed input', () => {
17+
expect(() => {
18+
new PodloveSimpleChapters('asdf');
19+
}).toThrowError(Error);
20+
});
21+
22+
const content = readFileSync(module.path + sep + 'samples' + sep + 'podlove-simple-chapters.xml', 'utf-8');
23+
24+
it('parses well-formed input', () => {
25+
expect(() => {
26+
new PodloveSimpleChapters(content);
27+
}).not.toThrow(Error);
28+
});
29+
30+
const instance = new PodloveSimpleChapters(content);
31+
32+
it('has the correct number of chapters from content', () => {
33+
expect(instance.chapters.length).toEqual(4);
34+
});
35+
36+
it('has parsed the timestamps correctly', () => {
37+
expect(instance.chapters[1].startTime).toBe(187)
38+
});
39+
40+
it('has parsed the chapter titles correctly', () => {
41+
expect(instance.chapters[0].title).toBe('Welcome')
42+
});
43+
44+
it('exports to correct format', () => {
45+
expect(instance.toString()).toContain('psc:chapters');
46+
});
47+
48+
it('export includes correct timestamp', () => {
49+
expect(instance.toString()).toContain('3:07');
50+
});
51+
52+
it('can import previously generated export', () => {
53+
expect(new PodloveSimpleChapters(instance.toString()).chapters[1].startTime).toEqual(187);
54+
});
55+
56+
it('can convert into other format', () => {
57+
expect(instance.to(FFMetadata)).toBeInstanceOf(FFMetadata)
58+
});
59+
60+
});
+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
3+
<channel>
4+
<title>Podlove Podcast</title>
5+
<atom:link type="text/html" href="http://podlove.org/" />
6+
<item>
7+
<title>Fiat Lux</title>
8+
<link href="http://podlove.org/podcast/1"/>
9+
<guid isPermaLink="false">urn:uuid:3241ace2-ca21-dd12-2341-1412ce31fad2</guid>
10+
<pubDate>Fri, 23 Mar 2012 23:25:19 +0000</pubDate>
11+
<description>First episode</description>
12+
<link rel="enclosure" type="audio/mpeg" length="12345" href="http://podlove.org/files/fiatlux.mp3"/>
13+
<!-- specify chapter information -->
14+
<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">
15+
<psc:chapter start="0" title="Welcome" />
16+
<psc:chapter start="3:07" title="Introducing Podlove" href="http://podlove.org/" />
17+
<psc:chapter start="8:26.250" title="Podlove WordPress Plugin" href="http://podlove.org/podlove-podcast-publisher" />
18+
<psc:chapter start="12:42" title="Resumée" />
19+
</psc:chapters>
20+
</item>
21+
</channel>
22+
</rss>

wtf.xml

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
2+
<channel>
3+
<!-- this is only a fragment of an rss feed, see -->
4+
<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->
5+
<!-- for more information -->
6+
<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">
7+
<psc:chapter start="0" title="Welcome"/>
8+
<psc:chapter start="3:07" title="Introducing Podlove" href="http://podlove.org/"/>
9+
<psc:chapter start="8:26.250" title="Podlove WordPress Plugin" href="http://podlove.org/podlove-podcast-publisher"/>
10+
<psc:chapter start="12:42" title="Resumée"/>
11+
</psc:chapters>
12+
</channel>
13+
</rss>

0 commit comments

Comments
 (0)