Skip to content

Commit

Permalink
Merge pull request #8 from Mtillmann/psc-etc
Browse files Browse the repository at this point in the history
Podlove simple Chapters
  • Loading branch information
Mtillmann committed Jan 30, 2024
2 parents 6d94e57 + f3ed377 commit 052d435
Show file tree
Hide file tree
Showing 8 changed files with 254 additions and 6 deletions.
5 changes: 4 additions & 1 deletion src/Formats/AutoFormat.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { VorbisComment } from "./VorbisComment.js";
import { WebVTT } from "./WebVTT.js";
import { Youtube } from "./Youtube.js";
import { ShutterEDL } from "./ShutterEDL.js";
import { PodloveSimpleChapters } from "./PodloveSimpleChapters.js";

export const AutoFormat = {
classMap: {
Expand All @@ -24,7 +25,8 @@ export const AutoFormat = {
pyscenedetect: PySceneDetect,
vorbiscomment: VorbisComment,
applechapters: AppleChapters,
shutteredl: ShutterEDL
shutteredl: ShutterEDL,
psc: PodloveSimpleChapters
},

detect(inputString, returnWhat = 'instance') {
Expand All @@ -45,6 +47,7 @@ export const AutoFormat = {
}
}
} catch (e) {
//console.log(e);
//do nothing
}
});
Expand Down
103 changes: 103 additions & 0 deletions src/Formats/PodloveSimpleChapters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { FormatBase } from "./FormatBase.js";
import jsdom from "jsdom";
import { NPTToSeconds, secondsToNPT } from "../util.js";

export class PodloveSimpleChapters extends FormatBase {

supportsPrettyPrint = true;
filename = 'podlove-simple-chapters-fragment.xml';
mimeType = 'text/xml';

detect(inputString) {

return /<psc:chapters/.test(inputString);
}

parse(string) {
if (!this.detect(string)) {
throw new Error('Input must contain <psc:chapters ...> node');
}

let dom;
if (typeof DOMParser !== 'undefined') {
dom = (new DOMParser()).parseFromString(string, 'application/xml');
} else {
const { JSDOM } = jsdom;
dom = new JSDOM(string, { contentType: 'application/xml' });
dom = dom.window.document;
}


this.chapters = [...dom.querySelectorAll('[start]')].reduce((acc, node) => {

if (node.tagName === 'psc:chapter') {
const start = node.getAttribute('start');
const title = node.getAttribute('title');
const image = node.getAttribute('image');
const href = node.getAttribute('href');

const chapter = {
startTime: NPTToSeconds(start)
}

if (title) {
chapter.title = title;
}
if (image) {
chapter.img = image;
}
if (href) {
//is this ever used, except for this format?
chapter.href = href;
}

acc.push(chapter);
}
return acc;

}, []);

}

toString(pretty = false) {
const indent = (depth, string, spacesPerDepth = 2) => (pretty ? ' '.repeat(depth * spacesPerDepth) : '') + string;

let output = [
'<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
indent(1, '<channel>'),
indent(2, '<!-- this is only a fragment of an rss feed, see -->'),
indent(2, '<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->'),
indent(2, '<!-- for more information -->'),
indent(2, '<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">'),
];

this.chapters.forEach(chapter => {

const node = [
`<psc:chapter start="${secondsToNPT(chapter.startTime)}"`,
];

if (chapter.title) {
node.push(` title="${chapter.title}"`);
}
if (chapter.img) {
node.push(` image="${chapter.img}"`);
}
if (chapter.href) {
node.push(` href="${chapter.href}"`);
}
node.push('/>');

output.push(indent(3, node.join('')));

});

output.push(
indent(2, '</psc:chapters>'),
indent(1, '</channel>'),
indent(0, '</rss>')
);

return output.join(pretty ? "\n" : '');
}
}
4 changes: 1 addition & 3 deletions src/Formats/ShutterEDL.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ export class ShutterEDL extends FormatBase {
return acc;
}

console.log(startTime, endTime, title);

acc.push({
startTime,
endTime,
Expand All @@ -56,7 +54,7 @@ export class ShutterEDL extends FormatBase {

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

Expand Down
50 changes: 49 additions & 1 deletion src/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export function zeroPad(num, len = 3) {
}

export function secondsToTimestamp(s, options = {}) {
options = {...{hours: true, milliseconds: false}, ...options};
options = { ...{ hours: true, milliseconds: false }, ...options };

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

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

/**
* Converts a NPT (normal play time) to seconds, used by podlove simple chapters
*/
export function NPTToSeconds(npt) {
let [parts, ms] = npt.split('.');
ms = parseInt(ms || 0);
parts = parts.split(':');

while (parts.length < 3) {
parts.unshift(0);
}

let [hours, minutes, seconds] = parts.map(i => parseInt(i));

return timestampToSeconds(`${zeroPad(hours, 2)}:${zeroPad(minutes, 2)}:${zeroPad(seconds, 2)}.${zeroPad(ms, 3)}`);
}

export function secondsToNPT(seconds) {

if (seconds === 0) {
return '0';
}

const regularTimestamp = secondsToTimestamp(seconds, { milliseconds: true });
let [hoursAndMinutesAndSeconds, milliseconds] = regularTimestamp.split('.');
let [hours, minutes, secondsOnly] = hoursAndMinutesAndSeconds.split(':').map(i => parseInt(i));

if (milliseconds === '000') {
milliseconds = '';
} else {
milliseconds = '.' + milliseconds;
}

if (hours === 0 && minutes === 0) {
return `${secondsOnly}${milliseconds}`;
}

secondsOnly = zeroPad(secondsOnly, 2);

if(hours === 0){
return `${minutes}:${secondsOnly}${milliseconds}`;
}

minutes = zeroPad(minutes, 2);

return `${hours}:${minutes}:${secondsOnly}${milliseconds}`;
}

export function timestampToSeconds(timestamp, fixedString = false) {
let [seconds, minutes, hours] = timestamp.split(':').reverse();
let milliseconds = 0;
Expand Down
3 changes: 2 additions & 1 deletion tests/conversions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ 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 { 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];
const formats = [ChaptersJson, WebVTT, Youtube, FFMetadata, MatroskaXML, MKVMergeXML, MKVMergeSimple, PySceneDetect, AppleChapters, ShutterEDL, VorbisComment, PodloveSimpleChapters];

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

Expand Down
60 changes: 60 additions & 0 deletions tests/format_psc.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import { readFileSync } from "fs";
import { sep } from "path";
import { FFMetadata } from "../src/Formats/FFMetadata.js";
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";


describe('PodloveSimpleChapters Format Handler', () => {
it('accepts no arguments', () => {
expect(() => {
new PodloveSimpleChapters();
}).not.toThrowError(TypeError);
});


it('fails on malformed input', () => {
expect(() => {
new PodloveSimpleChapters('asdf');
}).toThrowError(Error);
});

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

it('parses well-formed input', () => {
expect(() => {
new PodloveSimpleChapters(content);
}).not.toThrow(Error);
});

const instance = new PodloveSimpleChapters(content);

it('has the correct number of chapters from content', () => {
expect(instance.chapters.length).toEqual(4);
});

it('has parsed the timestamps correctly', () => {
expect(instance.chapters[1].startTime).toBe(187)
});

it('has parsed the chapter titles correctly', () => {
expect(instance.chapters[0].title).toBe('Welcome')
});

it('exports to correct format', () => {
expect(instance.toString()).toContain('psc:chapters');
});

it('export includes correct timestamp', () => {
expect(instance.toString()).toContain('3:07');
});

it('can import previously generated export', () => {
expect(new PodloveSimpleChapters(instance.toString()).chapters[1].startTime).toEqual(187);
});

it('can convert into other format', () => {
expect(instance.to(FFMetadata)).toBeInstanceOf(FFMetadata)
});

});
22 changes: 22 additions & 0 deletions tests/samples/podlove-simple-chapters.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<title>Podlove Podcast</title>
<atom:link type="text/html" href="http://podlove.org/" />
<item>
<title>Fiat Lux</title>
<link href="http://podlove.org/podcast/1"/>
<guid isPermaLink="false">urn:uuid:3241ace2-ca21-dd12-2341-1412ce31fad2</guid>
<pubDate>Fri, 23 Mar 2012 23:25:19 +0000</pubDate>
<description>First episode</description>
<link rel="enclosure" type="audio/mpeg" length="12345" href="http://podlove.org/files/fiatlux.mp3"/>
<!-- specify chapter information -->
<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">
<psc:chapter start="0" title="Welcome" />
<psc:chapter start="3:07" title="Introducing Podlove" href="http://podlove.org/" />
<psc:chapter start="8:26.250" title="Podlove WordPress Plugin" href="http://podlove.org/podlove-podcast-publisher" />
<psc:chapter start="12:42" title="Resumée" />
</psc:chapters>
</item>
</channel>
</rss>
13 changes: 13 additions & 0 deletions wtf.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
<channel>
<!-- this is only a fragment of an rss feed, see -->
<!-- https://podlove.org/simple-chapters/#:~:text=37%20seconds-,Embedding%20Example,-This%20is%20an -->
<!-- for more information -->
<psc:chapters version="1.2" xmlns:psc="http://podlove.org/simple-chapters">
<psc:chapter start="0" title="Welcome"/>
<psc:chapter start="3:07" title="Introducing Podlove" href="http://podlove.org/"/>
<psc:chapter start="8:26.250" title="Podlove WordPress Plugin" href="http://podlove.org/podlove-podcast-publisher"/>
<psc:chapter start="12:42" title="Resumée"/>
</psc:chapters>
</channel>
</rss>

0 comments on commit 052d435

Please sign in to comment.