From f32877ab0fb2ef18e3c4e6b0ad52951eb22e6550 Mon Sep 17 00:00:00 2001 From: Martin Tillmann Date: Tue, 30 Jan 2024 12:55:57 +0100 Subject: [PATCH 1/3] Add PodloveSimpleChapters format and NPT conversion functions --- src/Formats/AutoFormat.js | 5 +- src/Formats/PodloveSimpleChapters.js | 102 ++++++++++++++++++++++ src/util.js | 50 ++++++++++- tests/samples/podlove-simple-chapters.xml | 22 +++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/Formats/PodloveSimpleChapters.js create mode 100644 tests/samples/podlove-simple-chapters.xml diff --git a/src/Formats/AutoFormat.js b/src/Formats/AutoFormat.js index 42db516..0d9a75f 100644 --- a/src/Formats/AutoFormat.js +++ b/src/Formats/AutoFormat.js @@ -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: { @@ -24,7 +25,8 @@ export const AutoFormat = { pyscenedetect: PySceneDetect, vorbiscomment: VorbisComment, applechapters: AppleChapters, - shutteredl: ShutterEDL + shutteredl: ShutterEDL, + psc: PodloveSimpleChapters }, detect(inputString, returnWhat = 'instance') { @@ -45,6 +47,7 @@ export const AutoFormat = { } } } catch (e) { + //console.log(e); //do nothing } }); diff --git a/src/Formats/PodloveSimpleChapters.js b/src/Formats/PodloveSimpleChapters.js new file mode 100644 index 0000000..ed592a3 --- /dev/null +++ b/src/Formats/PodloveSimpleChapters.js @@ -0,0 +1,102 @@ +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 / 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 = [ + '', + indent(0,''), + indent(1,''), + indent(1,''), + indent(1,''), + indent(1,''), + ]; + + this.chapters.forEach(chapter => { + + const node = [ + `'); + + output.push(indent(2, node.join(''))); + + }); + + output.push( + indent(1, ''), + indent(0, '') + ); + + return output.join(pretty ? "\n" : ''); + } +} \ No newline at end of file diff --git a/src/util.js b/src/util.js index dea314e..ffcf071 100644 --- a/src/util.js +++ b/src/util.js @@ -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(); @@ -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; diff --git a/tests/samples/podlove-simple-chapters.xml b/tests/samples/podlove-simple-chapters.xml new file mode 100644 index 0000000..c83488e --- /dev/null +++ b/tests/samples/podlove-simple-chapters.xml @@ -0,0 +1,22 @@ + + + + Podlove Podcast + + + Fiat Lux + + urn:uuid:3241ace2-ca21-dd12-2341-1412ce31fad2 + Fri, 23 Mar 2012 23:25:19 +0000 + First episode + + + + + + + + + + + \ No newline at end of file From 3775b5c7b2bd43ec9b075ca73f20d8c6b2ea1579 Mon Sep 17 00:00:00 2001 From: Martin Tillmann Date: Tue, 30 Jan 2024 13:14:32 +0100 Subject: [PATCH 2/3] Fix formatting and indentation issues in PodloveSimpleChapters.js and ShutterEDL.js --- src/Formats/PodloveSimpleChapters.js | 23 ++++++----- src/Formats/ShutterEDL.js | 2 - tests/conversions.test.js | 3 +- tests/format_psc.test.js | 60 ++++++++++++++++++++++++++++ wtf.xml | 13 ++++++ 5 files changed, 87 insertions(+), 14 deletions(-) create mode 100644 tests/format_psc.test.js create mode 100644 wtf.xml diff --git a/src/Formats/PodloveSimpleChapters.js b/src/Formats/PodloveSimpleChapters.js index ed592a3..c14aad9 100644 --- a/src/Formats/PodloveSimpleChapters.js +++ b/src/Formats/PodloveSimpleChapters.js @@ -35,7 +35,7 @@ export class PodloveSimpleChapters extends FormatBase { const title = node.getAttribute('title'); const image = node.getAttribute('image'); const href = node.getAttribute('href'); - + const chapter = { startTime: NPTToSeconds(start) } @@ -46,7 +46,7 @@ export class PodloveSimpleChapters extends FormatBase { if (image) { chapter.img = image; } - if (href){ + if (href) { //is this ever used, except for this format? chapter.href = href; } @@ -64,11 +64,11 @@ export class PodloveSimpleChapters extends FormatBase { let output = [ '', - indent(0,''), - indent(1,''), - indent(1,''), - indent(1,''), - indent(1,''), + indent(1, ''), + indent(2, ''), + indent(2, ''), + indent(2, ''), + indent(2, ''), ]; this.chapters.forEach(chapter => { @@ -88,13 +88,14 @@ export class PodloveSimpleChapters extends FormatBase { } node.push('/>'); - output.push(indent(2, node.join(''))); - + output.push(indent(3, node.join(''))); + }); output.push( - indent(1, ''), - indent(0, '') + indent(2, ''), + indent(1, ''), + indent(0, '') ); return output.join(pretty ? "\n" : ''); diff --git a/src/Formats/ShutterEDL.js b/src/Formats/ShutterEDL.js index 6b3f867..e0f6166 100644 --- a/src/Formats/ShutterEDL.js +++ b/src/Formats/ShutterEDL.js @@ -43,8 +43,6 @@ export class ShutterEDL extends FormatBase { return acc; } - console.log(startTime, endTime, title); - acc.push({ startTime, endTime, diff --git a/tests/conversions.test.js b/tests/conversions.test.js index 7ab6b86..f92da5b 100644 --- a/tests/conversions.test.js +++ b/tests/conversions.test.js @@ -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'); diff --git a/tests/format_psc.test.js b/tests/format_psc.test.js new file mode 100644 index 0000000..c49e969 --- /dev/null +++ b/tests/format_psc.test.js @@ -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) + }); + +}); diff --git a/wtf.xml b/wtf.xml new file mode 100644 index 0000000..dd4cfbb --- /dev/null +++ b/wtf.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + From f3ed377cc4fba11a132c6f3c49bee20744e2be45 Mon Sep 17 00:00:00 2001 From: Martin Tillmann Date: Tue, 30 Jan 2024 13:15:04 +0100 Subject: [PATCH 3/3] Refactor audio tracks in ShutterEDL format --- src/Formats/ShutterEDL.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Formats/ShutterEDL.js b/src/Formats/ShutterEDL.js index e0f6166..88f360b 100644 --- a/src/Formats/ShutterEDL.js +++ b/src/Formats/ShutterEDL.js @@ -54,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) => {