Skip to content

Commit

Permalink
Merge pull request #9 from Mtillmann/psc-etc
Browse files Browse the repository at this point in the history
adds Podlove XML and JSON and mp4chaps and improves testing
  • Loading branch information
Mtillmann committed Jan 30, 2024
2 parents 052d435 + 6a77b80 commit 6fa1399
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 71 deletions.
2 changes: 1 addition & 1 deletion readme.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# chaptertool

Create and convert chapters for podcasts, youtube, matroska, mkvmerge/nero/vorbis, webvtt, ffmpeg and apple chapters.
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.

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

Expand Down
8 changes: 7 additions & 1 deletion src/Formats/AutoFormat.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { WebVTT } from "./WebVTT.js";
import { Youtube } from "./Youtube.js";
import { ShutterEDL } from "./ShutterEDL.js";
import { PodloveSimpleChapters } from "./PodloveSimpleChapters.js";
import { MP4Chaps } from "./MP4Chaps.js";
import { PodloveJson } from "./PodloveJson.js";

export const AutoFormat = {
classMap: {
Expand All @@ -26,7 +28,9 @@ export const AutoFormat = {
vorbiscomment: VorbisComment,
applechapters: AppleChapters,
shutteredl: ShutterEDL,
psc: PodloveSimpleChapters
psc: PodloveSimpleChapters,
mp4chaps: MP4Chaps,
podlovejson: PodloveJson
},

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



return detected;
},

Expand Down
46 changes: 46 additions & 0 deletions src/Formats/MP4Chaps.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { FormatBase } from "./FormatBase.js";
import { secondsToTimestamp, timestampToSeconds } from "../util.js";

export class MP4Chaps extends FormatBase {

filename = 'mp4chaps.txt';
mimeType = 'text/plain';

detect(inputString) {
return /^\d\d:\d\d:\d\d.\d\d?\d?\s/.test(inputString.trim());
}

parse(string) {
if (!this.detect(string)) {
throw new Error('MP4Chaps *MUST* begin with 00:00:00, received: ' + string.substr(0, 10) + '...');
}
this.chapters = this.stringToLines(string).map(line => {
line = line.split(' ');
const startTime = timestampToSeconds(line.shift(line));
const [title, href] = line.join(' ').split('<');
const chapter = {
startTime,
title : title.trim()
}

if(href){
chapter.href = href.replace('>', '');
}

return chapter;
});
}

toString(){
return this.chapters.map((chapter) => {
const line = [];
line.push(secondsToTimestamp(chapter.startTime, {milliseconds: true}));
line.push(chapter.title);
if(chapter.href){
line.push(`<${chapter.href}>`);
}
return line.join(' ');
}).join("\n");
}

}
74 changes: 74 additions & 0 deletions src/Formats/PodloveJson.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { secondsToTimestamp, timestampToSeconds } from '../util.js';
import { FormatBase } from './FormatBase.js';


export class PodloveJson extends FormatBase{

filename = 'podlove-chapters.json';
mimeType = 'application/json';

detect(inputString) {
try {
const data = JSON.parse(inputString);
const { errors } = this.test(data);
if (errors.length > 0) {
throw new Error('data test failed');
}
} catch (e) {
return false;
}
return true;
}

test(data) {
if (!Array.isArray(data)) {
return { errors: ['JSON Structure: must be an array'] };
}

if (data.length === 0) {
return { errors: ['JSON Structure: must not be empty'] };
}

if(!data.every(chapter => 'start' in chapter)){
return { errors: ['JSON Structure: every chapter must have a start property'] };
}

return { errors: [] };
}


parse(string) {
const data = JSON.parse(string);
const {errors} = this.test(data);
if (errors.length > 0) {
throw new Error(errors.join(''));
}

this.chapters = data.map(raw => {
const {start, title, image, href} = raw;
const chapter = {
startTime: timestampToSeconds(start)
}
if(title){
chapter.title = title;
}
if(image){
chapter.img = image;
}
if(href){
chapter.href = href;
}
return chapter;
});
}

toString(pretty = false){
return JSON.stringify(this.chapters.map(chapter => ({
start: secondsToTimestamp(chapter.startTime, {milliseconds: true}),
title: chapter.title || '',
image: chapter.img || '',
href: chapter.href || ''
})), null, pretty ? 2 : 0);
}

}
4 changes: 2 additions & 2 deletions src/Formats/Youtube.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export class Youtube extends FormatBase {
mimeType = 'text/plain';

detect(inputString) {
return /^0?0:00/.test(inputString.trim());
return /^0?0:00(:00)?\s/.test(inputString.trim());
}

parse(string) {
if (!this.detect(string)) {
throw new Error('Youtube Chapters *MUST* begin with (0)0:00');
throw new Error('Youtube Chapters *MUST* begin with (0)0:00(:00), received: ' + string.substr(0, 10) + '...');
}
this.chapters = this.stringToLines(string).map(line => {
line = line.split(' ');
Expand Down
10 changes: 9 additions & 1 deletion tests/conversions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ import { AppleChapters } from "../src/Formats/AppleChapters.js";
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
import { VorbisComment } from "../src/Formats/VorbisComment.js";
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
import { PodloveJson } from "../src/Formats/PodloveJson.js";
import { readFileSync } from "fs";
import { sep } from "path";

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

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

Expand Down
67 changes: 26 additions & 41 deletions tests/format_autodetection.test.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,40 @@
import {ChaptersJson} from "../src/Formats/ChaptersJson.js";
import {WebVTT} from "../src/Formats/WebVTT.js";
import {Youtube} from "../src/Formats/Youtube.js";
import {FFMetadata} from "../src/Formats/FFMetadata.js";
import {MatroskaXML} from "../src/Formats/MatroskaXML.js";
import {MKVMergeXML} from "../src/Formats/MKVMergeXML.js";
import {MKVMergeSimple} from "../src/Formats/MKVMergeSimple.js";
import {readFileSync} from "fs";
import {sep} from "path";
import {FFMpegInfo} from "../src/Formats/FFMpegInfo.js";
import {AutoFormat} from "../src/Formats/AutoFormat.js";
import { ChaptersJson } from "../src/Formats/ChaptersJson.js";
import { WebVTT } from "../src/Formats/WebVTT.js";
import { Youtube } from "../src/Formats/Youtube.js";
import { FFMetadata } from "../src/Formats/FFMetadata.js";
import { MatroskaXML } from "../src/Formats/MatroskaXML.js";
import { MKVMergeXML } from "../src/Formats/MKVMergeXML.js";
import { MKVMergeSimple } from "../src/Formats/MKVMergeSimple.js";
import { readFileSync } from "fs";
import { sep } from "path";
import { FFMpegInfo } from "../src/Formats/FFMpegInfo.js";
import { AutoFormat } from "../src/Formats/AutoFormat.js";
import { AppleChapters } from "../src/Formats/AppleChapters.js";
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
import { VorbisComment } from "../src/Formats/VorbisComment.js";
import { PodloveJson } from "../src/Formats/PodloveJson.js";

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


const filesAndKeysAndHandlers = [
['applechapters.xml', 'applechapters', AppleChapters],
['chapters.json', 'chaptersjson', ChaptersJson],
['FFMetadata.txt', 'ffmetadata', FFMetadata],
['ffmpeginfo.txt', 'ffmpeginfo', FFMpegInfo],
['matroska.xml', 'matroskaxml', MatroskaXML],
['mkvmerge.simple.txt', 'mkvmergesimple', MKVMergeSimple],
['mkvmerge.xml', 'mkvmergexml', MKVMergeXML],
['mp4chaps.txt', 'mp4chaps', MP4Chaps],
['podlove-simple-chapters.xml', 'psc', PodloveSimpleChapters],
['podlove.json', 'podlovejson', PodloveJson],
['pyscenedetect.csv', 'pyscenedetect', PySceneDetect],
['shutter.edl', 'shutteredl', ShutterEDL],
['vorbiscomment.txt', 'vorbiscomment', VorbisComment],
['webvtt.txt', 'webvtt', WebVTT],
['youtube-chapters.txt', 'youtube', Youtube],
['youtube-chapters.txt', 'youtube', Youtube]
];

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

});


return;
/*
Object.entries(filesAndHAndlers).forEach(pair => {
const [file, className] = pair;
const content = readFileSync(module.path + sep + 'samples' + sep + file, 'utf-8');
it(`${className.name} detects ${file}`, () => {
expect(() => {
new className(content)
}).not.toThrow(Error);
});
Object.entries(filesAndHAndlers).forEach(pair => {
const className2 = pair[1];
if (className2 === className) {
return;
}
it(`${className2.name} rejects ${file}`, () => {
expect(() => {
new className2(content)
}).toThrow(Error);
});
})
});
*/
});
39 changes: 27 additions & 12 deletions tests/format_detection.test.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,46 @@
import {ChaptersJson} from "../src/Formats/ChaptersJson.js";
import {WebVTT} from "../src/Formats/WebVTT.js";
import {Youtube} from "../src/Formats/Youtube.js";
import {FFMetadata} from "../src/Formats/FFMetadata.js";
import {MatroskaXML} from "../src/Formats/MatroskaXML.js";
import {MKVMergeXML} from "../src/Formats/MKVMergeXML.js";
import {MKVMergeSimple} from "../src/Formats/MKVMergeSimple.js";
import {readFileSync} from "fs";
import {sep} from "path";
import { ChaptersJson } from "../src/Formats/ChaptersJson.js";
import { WebVTT } from "../src/Formats/WebVTT.js";
import { Youtube } from "../src/Formats/Youtube.js";
import { FFMetadata } from "../src/Formats/FFMetadata.js";
import { MatroskaXML } from "../src/Formats/MatroskaXML.js";
import { MKVMergeXML } from "../src/Formats/MKVMergeXML.js";
import { MKVMergeSimple } from "../src/Formats/MKVMergeSimple.js";
import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
import { AppleChapters } from "../src/Formats/AppleChapters.js";
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
import { VorbisComment } from "../src/Formats/VorbisComment.js";
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
import { PodloveJson } from "../src/Formats/PodloveJson.js";
import { readFileSync } from "fs";
import { sep } from "path";

describe('detection of input strings', () => {
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple];
const formats = [
ChaptersJson, WebVTT, Youtube, FFMetadata,
MatroskaXML, MKVMergeXML, MKVMergeSimple,
PySceneDetect, AppleChapters, ShutterEDL,
VorbisComment, PodloveSimpleChapters, MP4Chaps,
PodloveJson
];

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

const initial = new ChaptersJson(content);

formats.forEach(fromFormat => {
const from = initial.to(fromFormat).toString();

formats.forEach(toFormat => {

if(toFormat.name === fromFormat.name){
if (toFormat.name === fromFormat.name) {
it(`accepts output of ${fromFormat.name} given to ${toFormat.name}`, () => {

expect(() => {
new toFormat(from);
}).not.toThrow(Error);
});
}else{
} else {
it(`fails detection of ${fromFormat.name} output given to ${toFormat.name}`, () => {
expect(() => {
new toFormat(from);
Expand Down
Loading

0 comments on commit 6fa1399

Please sign in to comment.