Skip to content

Commit 631b6f4

Browse files
committed
Add AppleHLS format to AutoFormat and test files
1 parent f4a4d4f commit 631b6f4

8 files changed

+312
-4
lines changed

src/Formats/AppleHLS.js

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { secondsToTimestamp, timestampToSeconds } from '../util.js';
2+
import { FormatBase } from './FormatBase.js';
3+
4+
5+
export class AppleHLS extends FormatBase {
6+
7+
filename = 'apple-hls.json';
8+
mimeType = 'application/json';
9+
10+
titleLanguage = 'en';
11+
imageDims = [1280, 720];
12+
13+
detect(inputString) {
14+
try {
15+
const data = JSON.parse(inputString);
16+
const { errors } = this.test(data);
17+
if (errors.length > 0) {
18+
throw new Error('data test failed');
19+
}
20+
} catch (e) {
21+
return false;
22+
}
23+
return true;
24+
}
25+
26+
test(data) {
27+
if (!Array.isArray(data)) {
28+
return { errors: ['JSON Structure: must be an array'] };
29+
}
30+
31+
if (data.length === 0) {
32+
return { errors: ['JSON Structure: must not be empty'] };
33+
}
34+
35+
if (!data.every(chapter => 'chapter' in chapter && 'start-time' in chapter)) {
36+
return { errors: ['JSON Structure: every chapter must have a chapter and a start-time property'] };
37+
}
38+
39+
return { errors: [] };
40+
}
41+
42+
43+
parse(string) {
44+
const data = JSON.parse(string);
45+
const { errors } = this.test(data);
46+
if (errors.length > 0) {
47+
throw new Error(errors.join(''));
48+
}
49+
50+
this.chapters = data.map(raw => {
51+
const chapter = {
52+
startTime: parseFloat(raw['start-time'])
53+
}
54+
55+
if ('titles' in raw && raw.titles.length > 0) {
56+
chapter.title = raw.titles[0].title;
57+
}
58+
59+
if ('images' in raw && raw.images.length > 0) {
60+
chapter.img = raw.images[0].url;
61+
}
62+
63+
return chapter;
64+
});
65+
}
66+
67+
toString(pretty = false) {
68+
return JSON.stringify(this.chapters.map((c, i) => {
69+
70+
const chapter = {
71+
'start-time': c.startTime,
72+
chapter: i + 1,
73+
titles: [
74+
{
75+
title: c.title || `Chapter ${i + 1}`,
76+
language: this.titleLanguage
77+
}
78+
]
79+
}
80+
81+
if (c.img) {
82+
chapter.images = [
83+
{
84+
'image-category': "chapter",
85+
url: c.img,
86+
'pixel-width': this.imageDims[0],
87+
'pixel-height': this.imageDims[1]
88+
}
89+
]
90+
}
91+
92+
return chapter;
93+
}), null, pretty ? 2 : 0);
94+
}
95+
96+
}

src/Formats/AutoFormat.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { ShutterEDL } from "./ShutterEDL.js";
1313
import { PodloveSimpleChapters } from "./PodloveSimpleChapters.js";
1414
import { MP4Chaps } from "./MP4Chaps.js";
1515
import { PodloveJson } from "./PodloveJson.js";
16+
import { AppleHLS } from "./AppleHLS.js";
1617

1718
export const AutoFormat = {
1819
classMap: {
@@ -30,7 +31,8 @@ export const AutoFormat = {
3031
shutteredl: ShutterEDL,
3132
psc: PodloveSimpleChapters,
3233
mp4chaps: MP4Chaps,
33-
podlovejson: PodloveJson
34+
podlovejson: PodloveJson,
35+
applehls: AppleHLS
3436
},
3537

3638
detect(inputString, returnWhat = 'instance') {

tests/conversions.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { VorbisComment } from "../src/Formats/VorbisComment.js";
1212
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
1313
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
1414
import { PodloveJson } from "../src/Formats/PodloveJson.js";
15+
import { AppleHLS } from "../src/Formats/AppleHLS.js";
1516
import { readFileSync } from "fs";
1617
import { sep } from "path";
1718

@@ -21,7 +22,7 @@ describe('conversions from one format to any other', () => {
2122
MatroskaXML, MKVMergeXML, MKVMergeSimple,
2223
PySceneDetect, AppleChapters, ShutterEDL,
2324
VorbisComment, PodloveSimpleChapters, MP4Chaps,
24-
PodloveJson
25+
PodloveJson, AppleHLS
2526
];
2627

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

tests/format_applehls.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 { ChaptersJson } from "../src/Formats/ChaptersJson.js";
5+
import { AppleHLS } from "../src/Formats/AppleHLS.js";
6+
7+
8+
describe('AppleHLS Format Handler', () => {
9+
it('accepts no arguments', () => {
10+
expect(() => {
11+
new AppleHLS();
12+
}).not.toThrowError(TypeError);
13+
});
14+
15+
16+
it('fails on malformed input', () => {
17+
expect(() => {
18+
new AppleHLS('asdf');
19+
}).toThrowError(Error);
20+
});
21+
22+
const content = readFileSync(module.path + sep + 'samples' + sep + 'applehls.json', 'utf-8');
23+
24+
it('parses well-formed input', () => {
25+
expect(() => {
26+
new AppleHLS(content);
27+
}).not.toThrow(Error);
28+
});
29+
30+
const instance = new AppleHLS(content);
31+
32+
it('has the correct number of chapters from content', () => {
33+
expect(instance.chapters.length).toEqual(3);
34+
});
35+
36+
it('has parsed the timestamps correctly', () => {
37+
expect(instance.chapters[1].startTime).toBe(500.1)
38+
});
39+
40+
it('has parsed the chapter titles correctly', () => {
41+
expect(instance.chapters[0].title).toBe('birth')
42+
});
43+
44+
it('exports to correct format', () => {
45+
expect(instance.toString()).toContain('start-time":');
46+
});
47+
48+
it('export includes correct timestamp', () => {
49+
expect(instance.toString()).toContain('1200.2');
50+
});
51+
52+
it('can import previously generated export', () => {
53+
expect(new AppleHLS(instance.toString()).chapters[2].startTime).toEqual(1200.2);
54+
});
55+
56+
it('can convert into other format', () => {
57+
expect(instance.to(ChaptersJson)).toBeInstanceOf(ChaptersJson)
58+
});
59+
60+
});

tests/format_autodetection.test.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,13 @@ import { PySceneDetect } from "../src/Formats/PySceneDetect.js";
1616
import { ShutterEDL } from "../src/Formats/ShutterEDL.js";
1717
import { VorbisComment } from "../src/Formats/VorbisComment.js";
1818
import { PodloveJson } from "../src/Formats/PodloveJson.js";
19+
import { AppleHLS } from "../src/Formats/AppleHLS.js";
1920

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

2223
const filesAndKeysAndHandlers = [
2324
['applechapters.xml', 'applechapters', AppleChapters],
25+
['applehls.json', 'applehls', AppleHLS],
2426
['chapters.json', 'chaptersjson', ChaptersJson],
2527
['FFMetadata.txt', 'ffmetadata', FFMetadata],
2628
['ffmpeginfo.txt', 'ffmpeginfo', FFMpegInfo],
@@ -34,7 +36,7 @@ describe('autodetection of sample files', () => {
3436
['shutter.edl', 'shutteredl', ShutterEDL],
3537
['vorbiscomment.txt', 'vorbiscomment', VorbisComment],
3638
['webvtt.txt', 'webvtt', WebVTT],
37-
['youtube-chapters.txt', 'youtube', Youtube]
39+
['youtube-chapters.txt', 'youtube', Youtube],
3840
];
3941

4042
filesAndKeysAndHandlers.forEach(item => {

tests/format_detection.test.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { VorbisComment } from "../src/Formats/VorbisComment.js";
1212
import { PodloveSimpleChapters } from "../src/Formats/PodloveSimpleChapters.js";
1313
import { MP4Chaps } from "../src/Formats/MP4Chaps.js";
1414
import { PodloveJson } from "../src/Formats/PodloveJson.js";
15+
import { AppleHLS } from "../src/Formats/AppleHLS.js";
1516
import { readFileSync } from "fs";
1617
import { sep } from "path";
1718

@@ -21,7 +22,7 @@ describe('detection of input strings', () => {
2122
MatroskaXML, MKVMergeXML, MKVMergeSimple,
2223
PySceneDetect, AppleChapters, ShutterEDL,
2324
VorbisComment, PodloveSimpleChapters, MP4Chaps,
24-
PodloveJson
25+
PodloveJson, AppleHLS
2526
];
2627

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

tests/samples/applehls.json

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"chapter": 1,
4+
"start-time": 0,
5+
"titles": [
6+
{
7+
"language": "en",
8+
"title": "birth"
9+
},
10+
{
11+
"language": "es",
12+
"title": "nacimiento"
13+
}
14+
]
15+
},
16+
{
17+
"chapter": 2,
18+
"start-time": 500.1,
19+
"titles": [
20+
{
21+
"language": "en",
22+
"title": "life"
23+
},
24+
{
25+
"language": "es",
26+
"title": "vida"
27+
}
28+
]
29+
},
30+
{
31+
"chapter": 3,
32+
"start-time": 1200.2,
33+
"titles": [
34+
{
35+
"language": "en",
36+
"title": "death"
37+
},
38+
{
39+
"language": "es",
40+
"title": "muerte"
41+
}
42+
]
43+
}
44+
]
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "HLS Chapter Data",
4+
"description": "HLS chapter data format",
5+
"type": "array",
6+
"items": {
7+
"description": "chapter entry",
8+
"type": "object",
9+
"properties": {
10+
"chapter": {
11+
"description": "Chapter number (optional)",
12+
"type": "number",
13+
"minimum": 1
14+
},
15+
"start-time": {
16+
"description": "Chapter start time",
17+
"type": "number",
18+
"minimum": 0
19+
},
20+
"duration": {
21+
"description": "Chapter duration (optional)",
22+
"type": "number",
23+
"minimum": 0,
24+
"exclusiveMinimum": true
25+
},
26+
"titles": {
27+
"description": "List of titles by language for chapter (optional)",
28+
"type": "array",
29+
"items": {
30+
"description": "Title object",
31+
"type": "object",
32+
"properties": {
33+
"language": {
34+
"description": "BCP 47 language code; und for undefined",
35+
"type": "string"
36+
},
37+
"title": {
38+
"description": "Chapter title string",
39+
"type": "string"
40+
}
41+
},
42+
"required": ["language", "title"]
43+
}
44+
},
45+
"images": {
46+
"description": "List of images for chapter (optional)",
47+
"type": "array",
48+
"items": {
49+
"description": "Image object",
50+
"type": "object",
51+
"properties": {
52+
"image-category": {
53+
"description": "Image category",
54+
"type": "string"
55+
},
56+
"pixel-width": {
57+
"description": "Pixel width",
58+
"type": "integer",
59+
"minimum": 0,
60+
"exclusiveMinimum": true
61+
},
62+
"pixel-height": {
63+
"description": "Pixel height",
64+
"type": "integer",
65+
"minimum": 0,
66+
"exclusiveMinimum": true
67+
},
68+
"url": {
69+
"description": "URL to image (relative or absolute)",
70+
"type": "string"
71+
}
72+
},
73+
"required": ["image-category", "pixel-width", "pixel-height", "url"]
74+
}
75+
},
76+
"metadata": {
77+
"description": "List of metadata entries for chapter (optional)",
78+
"type": "array",
79+
"items": {
80+
"description": "Metadata object",
81+
"type": "object",
82+
"properties": {
83+
"key": {
84+
"description": "Key value name",
85+
"type": "string"
86+
},
87+
"value": {
88+
"description": "Metadata value",
89+
"type": ["string", "number", "boolean", "array", "object"]
90+
},
91+
"language": {
92+
"description": "BCP 47 language code (optional)",
93+
"type": "string"
94+
}
95+
},
96+
"required": ["key", "value"]
97+
}
98+
}
99+
},
100+
"required": ["start-time"]
101+
}
102+
}

0 commit comments

Comments
 (0)