Skip to content

Commit 6fa1399

Browse files
authored
Merge pull request #9 from Mtillmann/psc-etc
adds Podlove XML and JSON and mp4chaps and improves testing
2 parents 052d435 + 6a77b80 commit 6fa1399

13 files changed

+322
-71
lines changed

readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# chaptertool
22

3-
Create and convert chapters for podcasts, youtube, matroska, mkvmerge/nero/vorbis, webvtt, ffmpeg and apple chapters.
3+
Create and _convert_ chapters for podcasts, youtube, matroska, mkvmerge/nero/vorbis, webvtt, ffmpeginfo, ffmetadata, pyscenedetect, apple chapters, edl, podlove simple chapters (xml, json) and mp4chaps.
44

55
The cli tools can automatically create chapters with images from videos using ffmpeg's scene detection.
66

src/Formats/AutoFormat.js

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import { WebVTT } from "./WebVTT.js";
1111
import { Youtube } from "./Youtube.js";
1212
import { ShutterEDL } from "./ShutterEDL.js";
1313
import { PodloveSimpleChapters } from "./PodloveSimpleChapters.js";
14+
import { MP4Chaps } from "./MP4Chaps.js";
15+
import { PodloveJson } from "./PodloveJson.js";
1416

1517
export const AutoFormat = {
1618
classMap: {
@@ -26,7 +28,9 @@ export const AutoFormat = {
2628
vorbiscomment: VorbisComment,
2729
applechapters: AppleChapters,
2830
shutteredl: ShutterEDL,
29-
psc: PodloveSimpleChapters
31+
psc: PodloveSimpleChapters,
32+
mp4chaps: MP4Chaps,
33+
podlovejson: PodloveJson
3034
},
3135

3236
detect(inputString, returnWhat = 'instance') {
@@ -56,6 +60,8 @@ export const AutoFormat = {
5660
throw new Error('failed to detect type of given input :(')
5761
}
5862

63+
64+
5965
return detected;
6066
},
6167

src/Formats/MP4Chaps.js

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { FormatBase } from "./FormatBase.js";
2+
import { secondsToTimestamp, timestampToSeconds } from "../util.js";
3+
4+
export class MP4Chaps extends FormatBase {
5+
6+
filename = 'mp4chaps.txt';
7+
mimeType = 'text/plain';
8+
9+
detect(inputString) {
10+
return /^\d\d:\d\d:\d\d.\d\d?\d?\s/.test(inputString.trim());
11+
}
12+
13+
parse(string) {
14+
if (!this.detect(string)) {
15+
throw new Error('MP4Chaps *MUST* begin with 00:00:00, received: ' + string.substr(0, 10) + '...');
16+
}
17+
this.chapters = this.stringToLines(string).map(line => {
18+
line = line.split(' ');
19+
const startTime = timestampToSeconds(line.shift(line));
20+
const [title, href] = line.join(' ').split('<');
21+
const chapter = {
22+
startTime,
23+
title : title.trim()
24+
}
25+
26+
if(href){
27+
chapter.href = href.replace('>', '');
28+
}
29+
30+
return chapter;
31+
});
32+
}
33+
34+
toString(){
35+
return this.chapters.map((chapter) => {
36+
const line = [];
37+
line.push(secondsToTimestamp(chapter.startTime, {milliseconds: true}));
38+
line.push(chapter.title);
39+
if(chapter.href){
40+
line.push(`<${chapter.href}>`);
41+
}
42+
return line.join(' ');
43+
}).join("\n");
44+
}
45+
46+
}

src/Formats/PodloveJson.js

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { secondsToTimestamp, timestampToSeconds } from '../util.js';
2+
import { FormatBase } from './FormatBase.js';
3+
4+
5+
export class PodloveJson extends FormatBase{
6+
7+
filename = 'podlove-chapters.json';
8+
mimeType = 'application/json';
9+
10+
detect(inputString) {
11+
try {
12+
const data = JSON.parse(inputString);
13+
const { errors } = this.test(data);
14+
if (errors.length > 0) {
15+
throw new Error('data test failed');
16+
}
17+
} catch (e) {
18+
return false;
19+
}
20+
return true;
21+
}
22+
23+
test(data) {
24+
if (!Array.isArray(data)) {
25+
return { errors: ['JSON Structure: must be an array'] };
26+
}
27+
28+
if (data.length === 0) {
29+
return { errors: ['JSON Structure: must not be empty'] };
30+
}
31+
32+
if(!data.every(chapter => 'start' in chapter)){
33+
return { errors: ['JSON Structure: every chapter must have a start property'] };
34+
}
35+
36+
return { errors: [] };
37+
}
38+
39+
40+
parse(string) {
41+
const data = JSON.parse(string);
42+
const {errors} = this.test(data);
43+
if (errors.length > 0) {
44+
throw new Error(errors.join(''));
45+
}
46+
47+
this.chapters = data.map(raw => {
48+
const {start, title, image, href} = raw;
49+
const chapter = {
50+
startTime: timestampToSeconds(start)
51+
}
52+
if(title){
53+
chapter.title = title;
54+
}
55+
if(image){
56+
chapter.img = image;
57+
}
58+
if(href){
59+
chapter.href = href;
60+
}
61+
return chapter;
62+
});
63+
}
64+
65+
toString(pretty = false){
66+
return JSON.stringify(this.chapters.map(chapter => ({
67+
start: secondsToTimestamp(chapter.startTime, {milliseconds: true}),
68+
title: chapter.title || '',
69+
image: chapter.img || '',
70+
href: chapter.href || ''
71+
})), null, pretty ? 2 : 0);
72+
}
73+
74+
}

src/Formats/Youtube.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,12 @@ export class Youtube extends FormatBase {
77
mimeType = 'text/plain';
88

99
detect(inputString) {
10-
return /^0?0:00/.test(inputString.trim());
10+
return /^0?0:00(:00)?\s/.test(inputString.trim());
1111
}
1212

1313
parse(string) {
1414
if (!this.detect(string)) {
15-
throw new Error('Youtube Chapters *MUST* begin with (0)0:00');
15+
throw new Error('Youtube Chapters *MUST* begin with (0)0:00(:00), received: ' + string.substr(0, 10) + '...');
1616
}
1717
this.chapters = this.stringToLines(string).map(line => {
1818
line = line.split(' ');

tests/conversions.test.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,19 @@ import { AppleChapters } from "../src/Formats/AppleChapters.js";
1010
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
1111
import { VorbisComment } from "../src/Formats/VorbisComment.js";
1212
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
13+
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
14+
import { PodloveJson } from "../src/Formats/PodloveJson.js";
1315
import { readFileSync } from "fs";
1416
import { sep } from "path";
1517

1618
describe('conversions from one format to any other', () => {
17-
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple, PySceneDetect, AppleChapters, ShutterEDL, VorbisComment, PodloveSimpleChapters];
19+
const formats = [
20+
ChaptersJson, WebVTT, Youtube, FFMetadata,
21+
MatroskaXML, MKVMergeXML, MKVMergeSimple,
22+
PySceneDetect, AppleChapters, ShutterEDL,
23+
VorbisComment, PodloveSimpleChapters, MP4Chaps,
24+
PodloveJson
25+
];
1826

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

tests/format_autodetection.test.js

+26-41
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
1-
import {ChaptersJson} from "../src/Formats/ChaptersJson.js";
2-
import {WebVTT} from "../src/Formats/WebVTT.js";
3-
import {Youtube} from "../src/Formats/Youtube.js";
4-
import {FFMetadata} from "../src/Formats/FFMetadata.js";
5-
import {MatroskaXML} from "../src/Formats/MatroskaXML.js";
6-
import {MKVMergeXML} from "../src/Formats/MKVMergeXML.js";
7-
import {MKVMergeSimple} from "../src/Formats/MKVMergeSimple.js";
8-
import {readFileSync} from "fs";
9-
import {sep} from "path";
10-
import {FFMpegInfo} from "../src/Formats/FFMpegInfo.js";
11-
import {AutoFormat} from "../src/Formats/AutoFormat.js";
1+
import { ChaptersJson } from "../src/Formats/ChaptersJson.js";
2+
import { WebVTT } from "../src/Formats/WebVTT.js";
3+
import { Youtube } from "../src/Formats/Youtube.js";
4+
import { FFMetadata } from "../src/Formats/FFMetadata.js";
5+
import { MatroskaXML } from "../src/Formats/MatroskaXML.js";
6+
import { MKVMergeXML } from "../src/Formats/MKVMergeXML.js";
7+
import { MKVMergeSimple } from "../src/Formats/MKVMergeSimple.js";
8+
import { readFileSync } from "fs";
9+
import { sep } from "path";
10+
import { FFMpegInfo } from "../src/Formats/FFMpegInfo.js";
11+
import { AutoFormat } from "../src/Formats/AutoFormat.js";
12+
import { AppleChapters } from "../src/Formats/AppleChapters.js";
13+
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
14+
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
15+
import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
16+
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
17+
import { VorbisComment } from "../src/Formats/VorbisComment.js";
18+
import { PodloveJson } from "../src/Formats/PodloveJson.js";
1219

1320
describe('autodetection of sample files', () => {
1421

15-
1622
const filesAndKeysAndHandlers = [
23+
['applechapters.xml', 'applechapters', AppleChapters],
1724
['chapters.json', 'chaptersjson', ChaptersJson],
1825
['FFMetadata.txt', 'ffmetadata', FFMetadata],
1926
['ffmpeginfo.txt', 'ffmpeginfo', FFMpegInfo],
2027
['matroska.xml', 'matroskaxml', MatroskaXML],
2128
['mkvmerge.simple.txt', 'mkvmergesimple', MKVMergeSimple],
2229
['mkvmerge.xml', 'mkvmergexml', MKVMergeXML],
30+
['mp4chaps.txt', 'mp4chaps', MP4Chaps],
31+
['podlove-simple-chapters.xml', 'psc', PodloveSimpleChapters],
32+
['podlove.json', 'podlovejson', PodloveJson],
33+
['pyscenedetect.csv', 'pyscenedetect', PySceneDetect],
34+
['shutter.edl', 'shutteredl', ShutterEDL],
35+
['vorbiscomment.txt', 'vorbiscomment', VorbisComment],
2336
['webvtt.txt', 'webvtt', WebVTT],
24-
['youtube-chapters.txt', 'youtube', Youtube],
37+
['youtube-chapters.txt', 'youtube', Youtube]
2538
];
2639

2740
filesAndKeysAndHandlers.forEach(item => {
@@ -62,32 +75,4 @@ describe('autodetection of sample files', () => {
6275

6376
});
6477

65-
66-
return;
67-
/*
68-
Object.entries(filesAndHAndlers).forEach(pair => {
69-
const [file, className] = pair;
70-
const content = readFileSync(module.path + sep + 'samples' + sep + file, 'utf-8');
71-
it(`${className.name} detects ${file}`, () => {
72-
expect(() => {
73-
new className(content)
74-
}).not.toThrow(Error);
75-
});
76-
77-
Object.entries(filesAndHAndlers).forEach(pair => {
78-
const className2 = pair[1];
79-
if (className2 === className) {
80-
return;
81-
}
82-
83-
it(`${className2.name} rejects ${file}`, () => {
84-
expect(() => {
85-
new className2(content)
86-
}).toThrow(Error);
87-
});
88-
})
89-
});
90-
91-
92-
*/
9378
});

tests/format_detection.test.js

+27-12
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,46 @@
1-
import {ChaptersJson} from "../src/Formats/ChaptersJson.js";
2-
import {WebVTT} from "../src/Formats/WebVTT.js";
3-
import {Youtube} from "../src/Formats/Youtube.js";
4-
import {FFMetadata} from "../src/Formats/FFMetadata.js";
5-
import {MatroskaXML} from "../src/Formats/MatroskaXML.js";
6-
import {MKVMergeXML} from "../src/Formats/MKVMergeXML.js";
7-
import {MKVMergeSimple} from "../src/Formats/MKVMergeSimple.js";
8-
import {readFileSync} from "fs";
9-
import {sep} from "path";
1+
import { ChaptersJson } from "../src/Formats/ChaptersJson.js";
2+
import { WebVTT } from "../src/Formats/WebVTT.js";
3+
import { Youtube } from "../src/Formats/Youtube.js";
4+
import { FFMetadata } from "../src/Formats/FFMetadata.js";
5+
import { MatroskaXML } from "../src/Formats/MatroskaXML.js";
6+
import { MKVMergeXML } from "../src/Formats/MKVMergeXML.js";
7+
import { MKVMergeSimple } from "../src/Formats/MKVMergeSimple.js";
8+
import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
9+
import { AppleChapters } from "../src/Formats/AppleChapters.js";
10+
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
11+
import { VorbisComment } from "../src/Formats/VorbisComment.js";
12+
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
13+
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
14+
import { PodloveJson } from "../src/Formats/PodloveJson.js";
15+
import { readFileSync } from "fs";
16+
import { sep } from "path";
1017

1118
describe('detection of input strings', () => {
12-
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple];
19+
const formats = [
20+
ChaptersJson, WebVTT, Youtube, FFMetadata,
21+
MatroskaXML, MKVMergeXML, MKVMergeSimple,
22+
PySceneDetect, AppleChapters, ShutterEDL,
23+
VorbisComment, PodloveSimpleChapters, MP4Chaps,
24+
PodloveJson
25+
];
1326

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

1629
const initial = new ChaptersJson(content);
1730

1831
formats.forEach(fromFormat => {
1932
const from = initial.to(fromFormat).toString();
33+
2034
formats.forEach(toFormat => {
2135

22-
if(toFormat.name === fromFormat.name){
36+
if (toFormat.name === fromFormat.name) {
2337
it(`accepts output of ${fromFormat.name} given to ${toFormat.name}`, () => {
38+
2439
expect(() => {
2540
new toFormat(from);
2641
}).not.toThrow(Error);
2742
});
28-
}else{
43+
} else {
2944
it(`fails detection of ${fromFormat.name} output given to ${toFormat.name}`, () => {
3045
expect(() => {
3146
new toFormat(from);

0 commit comments

Comments
 (0)