-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
180 lines (148 loc) · 4.98 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
const fs = require("fs").promises;
const rimraf = require("rimraf");
const { execSync } = require("child_process");
const fetch = require("node-fetch");
const parser = new (require("rss-parser"))();
const { createCanvas, loadImage } = require("canvas");
const { Command } = require("commander");
async function getFeedItem(i) {
console.log("Reading feed...");
const feed = await parser.parseURL("https://workingdraft.de/feed/");
console.log(`Proceeding with feed item "${feed.items[i].title}"...`);
return { title: feed.items[i].title, url: feed.items[i].enclosure.url };
}
async function downloadAudio({ title, url }) {
console.log(`Downloading audio...`);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Got status ${response.status} while fetching audio`);
}
const audioFile = `tmp/${title}.mp3`;
await fs.writeFile(audioFile, await response.buffer());
return audioFile;
}
function parseTitle(title) {
const match = /^Revision ([0-9]+)(?::(.+))?/.exec(title);
if (!match) {
console.warn(
`WARNING: Unable to parse title "${title}", check if generated thumbnail is ok`
);
return { nr: -1, text: null };
}
return { nr: match[1], text: match[2] };
}
async function generateThumbnail(title) {
console.log(`Rendering thumbnail...`);
const { nr, text } = parseTitle(title);
const tagText = nr > -1 ? `#${nr}` : `#spezial`;
const subText = nr === -1 || !text ? title : text;
const image = await loadImage("img/video.png");
const maxTextWidth = image.width - 2 * 250;
const canvas = createCanvas(image.width, image.height);
const ctx = canvas.getContext("2d");
ctx.drawImage(image, 0, 0);
ctx.font = "bold 400px Source Sans Pro";
ctx.textAlign = "left";
ctx.fillStyle = "#910c69";
ctx.strokeStyle = "#FFF";
ctx.lineWidth = 80;
ctx.strokeText(tagText, 250, canvas.height - 400);
ctx.fillText(tagText, 250, canvas.height - 400);
ctx.font = "bold 200px Source Sans Pro";
ctx.lineWidth = 40;
ctx.strokeText(subText, 250, canvas.height - 150, maxTextWidth);
ctx.fillText(subText, 250, canvas.height - 150, maxTextWidth);
const thumbnailFile = `tmp/${title}.png`;
await fs.writeFile(thumbnailFile, canvas.toBuffer("image/png"));
return thumbnailFile;
}
function encode(title, audioFile, thumbnailFile) {
const videoFile = `out/${title}.mov`;
console.log(`Encoding "${videoFile}". This will take some time...`);
execSync(
`ffmpeg -loop 1 -i "${thumbnailFile}" -i "${audioFile}" -tune stillimage -c:a aac -b:a 192k -pix_fmt yuv420p -shortest -preset ultrafast "${videoFile}" > log.txt`
);
return videoFile;
}
function parseArguments() {
const program = new Command();
program.version("1.0.0");
program
.option("-r, --revision", "Revisions-Eintrag")
.option("-t, --thumbnail <title>", "Generate a thumbnail only")
.option(
"-e, --entry <entry>",
"Welchen Eintrag im RSS Feed (0 = Letzte)",
0
)
.option("-k, --keep", "Outputverzeichnisse löschen", false)
.option("-h, --help", "Hilfe!")
.option(
"-m, --manual <entry...>",
'Manual processing. Pass audio file and title, e.g -m test.mp3 "Last Episode"'
);
program.parse(process.argv);
if (program.opts().help) {
program.outputHelp();
process.exit(0);
}
return program.opts();
}
function formatDate(ms) {
let s = parseInt(ms / 1000);
let min = parseInt(s / 60);
let hr = parseInt(min / 60);
s = s - min * 60;
min = min - hr * 60;
let str = "";
if(hr > 0) {
str += `${hr} hours `;
}
if(min > 0 || hr > 0) {
str += `${min} minutes `;
}
str += `${s} seconds`;
return str;
}
async function manual(options) {
if (options.manual.length !== 2) {
console.log("Insufficient number of arguments");
process.exit(1);
}
const [audioFile, title] = options.manual;
const thumbnailFile = await generateThumbnail(title);
return [audioFile, thumbnailFile, title];
}
async function feed(options) {
const { url, title } = await getFeedItem(parseInt(options.entry));
const [audioFile, thumbnailFile] = await Promise.all([
downloadAudio({ url, title }),
generateThumbnail(title),
]);
return [audioFile, thumbnailFile, title];
}
async function main() {
const start = Date.now();
const options = parseArguments();
console.log(options);
if (options.thumbnail) {
await generateThumbnail(options.thumbnail);
console.log(`Thumbnail rendered at ./tmp/${options.thumbnail}.png`);
}
if (!options.keep) {
rimraf.sync("out/*");
rimraf.sync("tmp/*");
}
// create something from disk (manual) or from the feed
let fn = options.manual ? manual : feed;
const [audioFile, thumbnailFile, title] = await fn(options);
const videoFile = encode(title, audioFile, thumbnailFile);
const end = Date.now();
console.log(`Took ${formatDate(end - start)}`);
return videoFile;
}
main().then((videoFile) => {
console.log(
`Done! Now upload "${videoFile}" to https://studio.youtube.com/channel/UCTJTfsq21-sC6maSTzifiPQ/videos/upload?d=ud`
);
});