Initial commit. Working empty project generation and file bin

This commit is contained in:
Kuba Orlik 2021-10-25 20:55:13 +02:00
parent 3d6ed22e5f
commit 13cd51fa28
10 changed files with 7440 additions and 6658 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ lib
coverage
@types
/.log/
/*.kdenlive

13692
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,34 @@
{
"name": "module-starter",
"version": "0.0.1",
"description": "module template",
"main": "lib/index.js",
"scripts": {
"test": "node test.js",
"build": "tsc",
"prepare": "npm run build",
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-with-tsc-error": "^0.0.7",
"mocha": "^9.1.1",
"mri": "^1.1.6",
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.19",
"ts-node": "^10.2.1",
"typescript": "^4.3.5"
},
"types": "./@types/index.d.ts"
"name": "kdenlive",
"version": "0.0.1",
"description": "Create kdenlive projects from within JS",
"main": "lib/index.js",
"scripts": {
"test": "node test.js",
"build": "tsc",
"prepare": "npm run build",
"test-reports": "npm run build && rm -fr .xunit coverage && npm run test -- --cover --test-report"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@types/mocha": "^9.0.0",
"@typescript-eslint/eslint-plugin": "^4.29.3",
"@typescript-eslint/parser": "^4.29.3",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-with-tsc-error": "^0.0.7",
"mocha": "^9.1.1",
"mri": "^1.1.6",
"nyc": "^15.1.0",
"prettier": "^2.3.2",
"source-map-support": "^0.5.19",
"ts-node": "^10.2.1",
"typescript": "^4.3.5"
},
"types": "./@types/index.d.ts",
"dependencies": {
"zx": "^4.2.0"
}
}

View File

@ -1,11 +1,61 @@
import { Example } from "./index";
import * as assert from "assert";
import { $ } from "zx";
import Project from "./kdenlive";
describe("Example", () => {
describe("Kdenlive", () => {
describe("example", () => {
it("should equal 'example'", () => {
const example = new Example();
assert.equal(example.example(), "example");
it("should equal 'example'", async () => {
const project = new Project(30);
project.addProducer("/home/kuba/Videos/5min.mp4");
project.addProducer("/home/kuba/Videos/5min.wav");
await $`echo ${await project.toXML()} > project-generated.kdenlive`;
});
});
describe("tracks", () => {
it("should generate a project with three audio tracks", async () => {
const project = new Project(30);
for (let i = 1; i <= 3; i++) {
project.addAudioTractor();
}
await $`echo ${await project.toXML()} > three-audio-tracks.kdenlive`;
});
it("should generate a project with one video track", async () => {
const project = new Project(30);
project.addVideoTractor();
await $`echo ${await project.toXML()} > 1v-tracks.kdenlive`;
});
it("should generate a project with one video track and one audio track", async () => {
const project = new Project(30);
project.addVideoTractor();
project.addAudioTractor();
await $`echo ${await project.toXML()} > 1a1v-tracks.kdenlive`;
});
it("should generate a project with three audio and video tracks", async () => {
const project = new Project(30);
for (let i = 1; i <= 3; i++) {
project.addVideoTractor();
project.addAudioTractor();
}
await $`echo ${await project.toXML()} > 3a3v-tracks.kdenlive`;
});
it("should generate a project with 20 audio and video tracks", async () => {
const project = new Project(30);
for (let i = 1; i <= 20; i++) {
project.addVideoTractor();
project.addAudioTractor();
}
await $`echo ${await project.toXML()} > 20a20v-tracks.kdenlive`;
});
});
});

View File

@ -1,5 +0,0 @@
export class Example {
example(): string {
return "example";
}
}

71
src/kdenlive.ts Normal file
View File

@ -0,0 +1,71 @@
import { BlackTrack, ConcreteProducer, Producer } from "./producer";
import { AudioTractor, trackIndexGen, Tractor, VideoTractor } from "./tractor";
export default class Project {
producers: Producer[] = [];
tractors: Tractor[] = [];
constructor(public fps: number) {
this.producers.push(new BlackTrack());
}
addProducer(file: string): ConcreteProducer {
const producer = new ConcreteProducer(file);
this.producers.push(producer);
return producer;
}
addAudioTractor(): AudioTractor {
const tractor = new AudioTractor();
this.tractors.push(tractor);
return tractor;
}
addVideoTractor(): AudioTractor {
const tractor = new VideoTractor();
this.tractors.push(tractor);
return tractor;
}
async toXML() {
return `<?xml version='1.0' encoding='utf-8'?>
<mlt LC_NUMERIC="C" producer="main_bin" version="7.0.0" root="/home/kuba/Downloads">
<profile frame_rate_num="${
this.fps
}" sample_aspect_num="1" display_aspect_den="9" colorspace="601" progressive="1" description="1920x1080 29.90fps" display_aspect_num="16" frame_rate_den="1" width="1920" height="1080" sample_aspect_den="1"/>
${(
await Promise.all(
this.producers.map((producer) => producer.toXML(this.fps))
)
).join("\n")}
<playlist id="main_bin">
<property name="kdenlive:docproperties.activeTrack">2</property>
<property name="kdenlive:docproperties.audioChannels">2</property>
<property name="kdenlive:docproperties.audioTarget">1</property>
<property name="kdenlive:docproperties.disablepreview">0</property>
<property name="kdenlive:docproperties.documentid">1633881496938</property>
<property name="kdenlive:docproperties.groups">[
]
</property>
<property name="kdenlive:docproperties.kdenliveversion">21.08.1</property>
<property name="kdenlive:docproperties.version">1.02</property>
<property name="kdenlive:expandedFolders"/>
<property name="kdenlive:documentnotes"/>
<property name="xml_retain">1</property>
${this.producers
.filter((e) => !(e instanceof BlackTrack))
.map((producer) => `<entry producer="producer${producer.index}"/>`)
.join("\n")}
</playlist>
${this.tractors.map((tractor) => tractor.toXML()).join("\n")}
<tractor id="tractor${
trackIndexGen.next().value
}" in="00:00:00.000" out="00:08:20.000">
<track producer="black_track"/>
${this.tractors
.map((tractor) => `<track producer="tractor${tractor.index}"/>`)
.join("\n")}
</tractor>
</mlt>
`;
}
}

23
src/playlist.ts Normal file
View File

@ -0,0 +1,23 @@
import { makeIDGen } from "./util";
const playlistIndexGen = makeIDGen(0);
export abstract class Playlist {
constructor(public index = playlistIndexGen.next().value) {}
abstract toXML(): string;
}
export class AudioPlaylist extends Playlist {
toXML() {
return /* HTML */ ` <playlist id="playlist${this.index}">
<property name="kdenlive:audio_track">1</property>
</playlist>`;
}
}
export class VideoPlaylist extends Playlist {
toXML() {
return /* HTML */ ` <playlist id="playlist${this.index}"></playlist>`;
}
}

90
src/producer.ts Normal file
View File

@ -0,0 +1,90 @@
import { makeIDGen } from "./util";
import { $ } from "zx";
const producerIndexGen = makeIDGen(0);
export abstract class Producer {
index: number;
constructor(public path: string) {
this.index = producerIndexGen.next().value;
}
async getNativeMltXml(fps: number) {
const xml = (
await $`melt ${
this.path
} -consumer xml ${`frame_rate_num=${fps}`} | htmlq producer`
).stdout;
return xml.replace("producer0", `producer${this.index}`);
}
async toXML(fps: number) {
return await this.getNativeMltXml(fps);
}
}
export class ConcreteProducer extends Producer {
video_only: VideoOnlyProducer;
audio_only: AudioOnlyProducer;
constructor(path: string) {
super(path);
this.video_only = new VideoOnlyProducer(path);
this.audio_only = new AudioOnlyProducer(path);
}
async toXML(fps: number) {
return (
await Promise.all([
super.toXML(fps),
this.video_only.toXML(fps),
this.audio_only.toXML(fps),
])
).join("\n");
}
}
class VideoOnlyProducer extends Producer {
async toXML(fps: number) {
const xml = await super.toXML(fps);
return xml.replace(
"</producer>",
` <property name="set.test_audio">0</property>
<property name="set.test_image">1</property>
</producer>`
);
}
}
class AudioOnlyProducer extends Producer {
async toXML(fps: number) {
const xml = await super.toXML(fps);
return xml.replace(
"</producer>",
` <property name="set.test_audio">1</property>
<property name="set.test_image">0</property>
</producer>`
);
}
}
export class BlackTrack extends Producer {
constructor() {
super("");
}
async toXML() {
return /* HTML */ `<producer
id="black_track"
in="00:00:00.000"
out="00:16:43.344"
>
<property name="length">2147483647</property>
<property name="eof">continue</property>
<property name="resource">black</property>
<property name="aspect_ratio">1</property>
<property name="mlt_service">color</property>
<property name="mlt_image_format">rgb24a</property>
<property name="set.test_audio">0</property>
</producer>`;
}
}

64
src/tractor.ts Normal file
View File

@ -0,0 +1,64 @@
import { AudioPlaylist, Playlist, VideoPlaylist } from "./playlist";
import { makeIDGen } from "./util";
export const trackIndexGen = makeIDGen(0);
export abstract class Tractor {
main_playlist: Playlist;
secondary_playlist: Playlist; // not sure what these are for, but Kdenlive generates them, sooo
public index = trackIndexGen.next().value;
abstract toXML(): string;
}
export class AudioTractor extends Tractor {
constructor() {
super();
this.main_playlist = new AudioPlaylist();
this.secondary_playlist = new AudioPlaylist();
}
toXML() {
return [
this.main_playlist.toXML(),
this.secondary_playlist.toXML(),
/* HTML */ ` <tractor id="tractor${this.index}" in="00:00:00.000">
<property name="kdenlive:audio_track">1</property>
<property name="kdenlive:trackheight">67</property>
<property name="kdenlive:timeline_active">1</property>
<property name="kdenlive:collapsed">0</property>
<property name="kdenlive:thumbs_format" />
<property name="kdenlive:audio_rec" />
<track
hide="video"
producer="playlist${this.main_playlist.index}"
/>
<track
hide="video"
producer="playlist${this.secondary_playlist.index}"
/>
</tractor>`,
].join("\n");
}
}
export class VideoTractor extends Tractor {
constructor() {
super();
this.main_playlist = new VideoPlaylist();
this.secondary_playlist = new VideoPlaylist();
}
toXML() {
return [
this.main_playlist.toXML(),
this.secondary_playlist.toXML(),
/* HTML */ ` <tractor id="tractor${this.index}" in="00:00:00.000">
<property name="kdenlive:trackheight">67</property>
<property name="kdenlive:timeline_active">1</property>
<track producer="playlist${this.main_playlist.index}" />
<track producer="playlist${this.secondary_playlist.index}" />
</tractor>`,
].join("\n");
}
}

28
src/util.ts Normal file
View File

@ -0,0 +1,28 @@
const HOUR = 60 * 60;
const MINUTE = 60;
export function formatDuration(float_s: number) {
const hours = Math.floor(float_s / HOUR);
float_s = float_s - hours * HOUR;
const minutes = Math.floor(float_s / MINUTE);
const seconds = float_s - minutes * MINUTE;
return `${twoDigits(hours)}:${twoDigits(minutes)}:${twoDigits(seconds, 5)}`;
}
export function twoDigits(number: number, decimal = 0) {
let [int, dec] = number.toFixed(decimal).split(".");
dec = dec || "";
if (dec == "") {
return int.padStart(2, "0");
} else {
return int.padStart(2, "0") + "." + dec;
}
}
export function* makeIDGen(first = 1): Generator<number, number> {
let i = first;
while (true) {
yield i;
i++;
}
}