Skip to content

Commit 9805a39

Browse files
committed
Rescue from dead laptop!
1 parent b563848 commit 9805a39

17 files changed

+1358
-246
lines changed

Colour.js

+1-3
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,7 @@ class Colour{
3030

3131
static writeColouredText(text, ...params){
3232
params = params.map(p => this.#getValue(p)).join('');
33-
console.log(params);
34-
console.log(text);
35-
console.log(this.#getValue(values.RESET));
33+
console.log(params + text + this.#getValue(values.RESET));
3634
}
3735

3836
static #getValue(escape){

ProcessManager.js

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
'use strict';
2+
3+
const { Worker } = require('node:worker_threads');
4+
const Colour = require('./Colour.js');
5+
6+
class ProcessManager {
7+
#workToDo = [];
8+
#threads = [];
9+
#config = {};
10+
#listeners = {};
11+
12+
//#stateCheckTimer = null;
13+
14+
constructor(workToDo, config) {
15+
console.log(workToDo);
16+
console.log(config);
17+
18+
this.#workToDo = workToDo;
19+
this.#config = config;
20+
21+
this.#initThreads();
22+
}
23+
24+
addEventListener(event, callback){
25+
event = event.toLowerCase();
26+
27+
if(!this.#listeners.hasOwnProperty(event)){
28+
this.#listeners[event] = [callback];
29+
} else {
30+
this.#listeners[event].push(callback);
31+
}
32+
}
33+
34+
run(){
35+
console.time("processing");
36+
this.#threads.forEach(f => {
37+
this.#assignWork(f);
38+
});
39+
40+
//this.#queryState();
41+
//clearInterval(this.#stateCheckTimer);
42+
//this.#stateCheckTimer = setInterval(() => {this.#queryState()}, 5000);
43+
}
44+
45+
//#queryState(){
46+
// this.#threads.forEach(t => {
47+
// t.postMessage({
48+
// type: 'STATUS'
49+
// });
50+
// });
51+
//}
52+
53+
#initThreads() {
54+
const count = this.#config.count ?? 3;
55+
for (let i = 0; i < count; i++) {
56+
const worker = new Worker('./Processor.js', this.#config);
57+
worker.on("error", (e) => { throw e });
58+
worker.on("message", msg => this.#handleWorkerResponse(worker, msg));
59+
60+
this.#threads.push(worker);
61+
}
62+
}
63+
64+
#assignWork(thread){
65+
if(this.#workToDo.length === 0){
66+
console.log("No work left");
67+
thread.postMessage({
68+
type: 'KILL'
69+
});
70+
71+
this.#threads.splice(this.#threads.indexOf(thread), 1);
72+
if(this.#threads.length === 0){
73+
this.#finish();
74+
}
75+
76+
return;
77+
}
78+
79+
const workItem = this.#workToDo.pop();
80+
workItem.type = 'FILE';
81+
thread.postMessage(workItem);
82+
}
83+
84+
#handleWorkerResponse(thread, msg){
85+
if(msg.type == 'DONE'){
86+
this.#assignWork(thread);
87+
} else if(msg.type == 'STATUS'){
88+
if(!msg.isWorking){
89+
Colour.writeColouredText("IDLE THREAD!", Colour.OPTIONS.FG_RED);
90+
} else {
91+
const duration = Math.round((performance.now() - msg.start) / 1000);
92+
Colour.writeColouredText(`Thread working on ${msg.job} (${duration}s)`, Colour.OPTIONS.FG_GREEN);
93+
}
94+
} else {
95+
console.log(msg);
96+
}
97+
}
98+
99+
#finish(){
100+
//clearInterval(this.#stateCheckTimer);
101+
console.timeEnd("processing");
102+
103+
if(this.#listeners?.hasOwnProperty("done")){
104+
this.#listeners["done"].forEach(l => {
105+
l();
106+
});
107+
}
108+
}
109+
}
110+
111+
module.exports = ProcessManager;

Processor.js

+264
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
1+
'use strict';
2+
3+
const { parentPort, workerData } = require('worker_threads');
4+
const sharp = require('sharp');
5+
const { WritableStream } = require('node:stream/web');
6+
const path = require('path');
7+
const fs = require('fs');
8+
9+
const { removeMany, removeFolder } = require('./utils');
10+
const VideoConverter = require('./VideoConverter');
11+
const Colour = require('./Colour');
12+
const Utils = require('./utils');
13+
14+
const today = new Date();
15+
const titleConfig = {
16+
width: 1920,
17+
height: 1080,
18+
title: `I&E ${today.getFullYear()}`
19+
};
20+
21+
const dlPath = workerData.dlPath;
22+
const config = workerData.config;
23+
24+
let isWorking = false;
25+
26+
async function processFiles(directory, files){
27+
if (!files || !files.value || files.value.length === 0) {
28+
return done();
29+
}
30+
31+
const videos = (await Promise.all([...files.value.map(f => processFile(dlPath, directory.name, f)), generateTitle({
32+
category: '',
33+
band: directory.name,
34+
name: titleConfig.title,
35+
width: titleConfig.width,
36+
height: titleConfig.height,
37+
path: path.join('tmp', `${directory.name}.jpg`)
38+
})])).filter(x => x);
39+
40+
const title = videos.pop();
41+
const withoutTitle = videos[0];
42+
videos[0] = await VideoConverter.addTitleToVideo(title, videos[0], 5, true);
43+
44+
if(videos[0].indexOf(":\\") < 0){
45+
videos[0] = path.join(__dirname, videos[0]);
46+
}
47+
48+
//4. Combine files
49+
const master = path.join('tmp', `${directory.name}.mp4`);
50+
if (videos.length === 0) {
51+
console.log();
52+
return '';
53+
}
54+
55+
if (videos.length === 1) {
56+
//rename only video to final video
57+
fs.renameSync(videos[0], master);
58+
} else {
59+
const tempJoin = await VideoConverter.combineCommonFormatVideos(...videos);
60+
fs.renameSync(tempJoin, master);
61+
}
62+
63+
removeMany([...videos, title, withoutTitle]);
64+
console.log();
65+
66+
return done(master);
67+
}
68+
69+
function logProcessStart(category, message){
70+
Colour.writeColouredText(`${category.trim().substring(0, 2)}: ${message}`, Colour.OPTIONS.FG_MAGENTA);
71+
}
72+
73+
function logProcessEnd(category, message){
74+
Colour.writeColouredText(`${category.trim().substring(0, 2)}: ${message}`, Colour.OPTIONS.FG_CYAN);
75+
}
76+
77+
async function processFile(dlPath, category, fileData) {
78+
if (!('file' in fileData)) {
79+
return '';
80+
}
81+
82+
const parts = path.parse(fileData.name).name.split(' - ');
83+
const name = parts[0].trim();
84+
const toRemove = [];
85+
let band = '';
86+
let trusted = false;
87+
88+
if (parts.length >= 2) {
89+
band = parts[1].trim();
90+
}
91+
92+
logProcessStart(category, `Downloading ${name} of ${band}, who has entered category ${category}...`);
93+
94+
//1. Download file
95+
dlPath = await downloadFile(dlPath, fileData, category.substring(0,2));
96+
97+
logProcessEnd(category, `Downloaded ${name} of ${band} to ${dlPath}`);
98+
99+
if(await VideoConverter.isPortrait(dlPath)){
100+
logProcessStart(category, `${name} of ${band} filmed in portrait - adding background to make landscape!`);
101+
toRemove.push(dlPath);
102+
dlPath = await VideoConverter.addLandscapeBackgroundToPortraitVideo(dlPath, "bg.png", titleConfig.height);
103+
logProcessEnd(category, `${name} of ${band} is now landscape`);
104+
trusted = true;
105+
}
106+
107+
//2. Generate title card
108+
logProcessStart(category, `Generating title card for ${name} of ${band}`);
109+
const title = await generateTitle({
110+
category: category?.trim() ?? '',
111+
band: band?.trim() ?? '',
112+
name: name?.trim() ?? '',
113+
width: titleConfig.width,
114+
height: titleConfig.height,
115+
path: dlPath + '.jpg'
116+
});
117+
logProcessEnd(category, `Generated title card for ${name} of ${band} at ${title}`);
118+
119+
//3. Add title card to video
120+
//yeahhh so this is where it gets horrible - we use ffmpeg here to do things!
121+
logProcessStart(category, `Adding title card to video entry of ${name} of ${band}`);
122+
const all = await VideoConverter.addTitleToVideo(title, dlPath, 5, trusted);
123+
toRemove.push(title, dlPath);
124+
logProcessEnd(category, `${name} of ${band} now has a title card!`);
125+
126+
logProcessStart(category, `Removing ${toRemove.length} temp files`);
127+
removeMany(toRemove);
128+
logProcessEnd(category, "Temporary files removed");
129+
130+
Colour.writeColouredText(`Finished processing ${name} of ${band} - final video is available at ${all}`, Colour.OPTIONS.FG_GREEN);
131+
return all;
132+
}
133+
134+
async function downloadFile(dlPath, fileData, prefix = '') {
135+
dlPath = path.join(dlPath, prefix + fileData.name);
136+
const res = await fetch(fileData['@microsoft.graph.downloadUrl']);
137+
const fileStream = fs.createWriteStream(dlPath);
138+
const stream = new WritableStream({
139+
write(chunk) {
140+
fileStream.write(chunk);
141+
}
142+
});
143+
144+
await res.body.pipeTo(stream);
145+
146+
return dlPath;
147+
}
148+
149+
async function generateTitle(opts) {
150+
const svgImage = `
151+
<svg width="${opts.width}" height="${opts.height}">
152+
<style>
153+
.name {
154+
fill: #000;
155+
font-size: 70px;
156+
font-weight: bold;
157+
font-family: 'Asket';
158+
}
159+
160+
.band, .category{
161+
fill: #000;
162+
font-family: 'Open Sans';
163+
}
164+
165+
.band {
166+
font-size: 70px;
167+
font-weight: lighter;
168+
}
169+
170+
.category{
171+
font-size: 50px;
172+
}
173+
</style>
174+
<text x="50%" y="35%" text-anchor="middle" class="name">${opts.name.toUpperCase().replaceAll('&', '&amp;')}</text>
175+
<text x="50%" y="50%" text-anchor="middle" font-style="italic" class="band">${opts.band.replaceAll('&', '&amp;')}</text>
176+
<text x="50%" y="65%" text-anchor="middle" class="category">${opts.category.replaceAll('&', '&amp;')}</text>
177+
</svg>
178+
`;
179+
180+
const buffer = Buffer.from(svgImage);
181+
182+
const byba = await sharp('byba.png').flatten({ background: '#FFF' }).resize({ height: opts.height / 2 }).ensureAlpha(0.4).toBuffer();
183+
const tymba = await sharp('TYMBA.png').flatten({ background: '#FFF' }).resize({ height: opts.height / 2 }).ensureAlpha(0.4).toBuffer();
184+
const sponsor = await sharp('marching arts.png').flatten({ background: '#FFF' }).resize({ height: opts.height * 0.1 }).toBuffer();
185+
186+
await sharp({
187+
create: {
188+
width: opts.width,
189+
height: opts.height,
190+
channels: 4,
191+
background: {
192+
r: 255,
193+
g: 255,
194+
b: 255,
195+
alpha: 1
196+
}
197+
}
198+
}).composite([
199+
{
200+
input: byba,
201+
top: opts.height / 4,
202+
left: -300
203+
},
204+
{
205+
input: tymba,
206+
top: opts.height / 4,
207+
left: opts.width - 270
208+
},
209+
{
210+
input: sponsor,
211+
left: opts.width * 0.4,
212+
top: opts.height * 0.85
213+
},
214+
{
215+
input: buffer,
216+
top: 0,
217+
left: 0
218+
}
219+
]).toFile(opts.path);
220+
221+
return opts.path;
222+
}
223+
224+
function done(mergedFile){
225+
isWorking = false;
226+
const msg = {
227+
type: "DONE"
228+
};
229+
230+
if(mergedFile != null){
231+
msg.file = mergedFile;
232+
}
233+
234+
parentPort.postMessage(msg);
235+
console.timeEnd(timerName);
236+
workStart = 0;
237+
}
238+
239+
let timerName = "";
240+
let workStart = 0;
241+
parentPort.on('message', async (msg) => {
242+
if(msg?.type && msg.type == "FILE"){
243+
timerName = msg.name;
244+
workStart = performance.now();
245+
console.time(timerName);
246+
247+
isWorking = true;
248+
const files = await Utils.listFiles(config, path.join(workerData.fileBase, msg.name))
249+
await processFiles(msg, files);
250+
251+
} else if(msg.type == "STATUS"){
252+
const data = {
253+
type: 'STATUS',
254+
isWorking
255+
};
256+
257+
if(isWorking){
258+
data.job = timerName,
259+
data.start = workStart
260+
};
261+
262+
parentPort.postMessage(data);
263+
}
264+
});

0 commit comments

Comments
 (0)