forked from serguun42/Social-Picker-API
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathugoira-builder.js
106 lines (91 loc) · 3.94 KB
/
ugoira-builder.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
import { exec } from 'child_process';
import { createHash } from 'crypto';
import { resolve } from 'path';
import { unlink, writeFile } from 'fs/promises';
import JSZip from 'jszip';
import LogMessageOrError from './log.js';
const TEMP_FOLDER = process.env.TEMP || '/tmp/';
const UGOIRA_FILE_EXTENSION = 'mp4';
/** @type {import('../types/social-post').Media['type']} */
const UGOIRA_MEDIA_FILETYPE = 'gif';
/**
* Builds video sequence from Ugoira
* @param {import('../types/pixiv-ugoira-meta').UgoiraMeta} ugoiraMeta
* @param {ArrayBuffer} sourceZip
* @returns {Promise<import('../types/social-post').Media>}
*/
const UgoiraBuilder = (ugoiraMeta, sourceZip) =>
new JSZip()
.loadAsync(sourceZip)
.then(async (zipAsObject) => {
/** @type {{ [filename: string]: number }} */
const ugoiraDelays = {};
ugoiraMeta.body.frames.forEach((frame) => {
ugoiraDelays[frame.file] = frame.delay;
});
const hash = createHash('md5').update(`${ugoiraMeta.body.originalSrc}_${Date.now()}`).digest('hex');
const outputFilename = `socialpicker_${hash}_output.${UGOIRA_FILE_EXTENSION}`;
const outputFilepath = resolve(TEMP_FOLDER, outputFilename);
/** @type {{ filename: string, tempFilename: string, tempFilepath: string }[]} */
const storedFiles = [];
// eslint-disable-next-line no-restricted-syntax, guard-for-in
for (const filename in zipAsObject.files) {
const fileAsObject = zipAsObject.files[filename];
const tempFilename = `socialpicker_${hash}_${filename.replace(/[^\w.]/g, '')}`;
const tempFilepath = resolve(TEMP_FOLDER, tempFilename);
// eslint-disable-next-line no-await-in-loop
const unzipWriteResult = await fileAsObject
.async('nodebuffer')
.then((fileAsBuffer) => writeFile(tempFilepath, fileAsBuffer))
.catch((e) => Promise.resolve(new Error(`Cannot unzip/write file ${filename}: ${e}`)));
if (unzipWriteResult instanceof Error) throw unzipWriteResult;
storedFiles.push({ filename, tempFilename, tempFilepath });
}
const listFilename = `socialpicker_${hash}_list.txt`;
const listFilepath = resolve(TEMP_FOLDER, listFilename);
const listContent = storedFiles
.map(
(storedFile) =>
`file '${storedFile.tempFilename}'\nduration ${((ugoiraDelays[storedFile.filename] || 100) / 1000).toFixed(
3
)}`
)
.join('\n');
return writeFile(listFilepath, listContent)
.then(
() =>
new Promise((ffmpegResolve, ffmpegReject) => {
const ffmpegProcess = exec(
`ffmpeg -f concat -i "${listFilename}" -vf format=yuv420p "${outputFilename}"`,
{ cwd: TEMP_FOLDER },
(error, _stdout, stderr) => {
if (error || stderr) {
ffmpegProcess.kill();
ffmpegReject(error || new Error(stderr));
}
}
);
ffmpegProcess.on('error', (e) => ffmpegReject(e));
ffmpegProcess.on('exit', () => ffmpegResolve());
})
)
.then(() => {
unlink(listFilepath).catch(() => {});
storedFiles.forEach((storedFile) => unlink(storedFile.tempFilepath).catch(() => {}));
/** @type {import('../types/social-post').Media} */
const ugoiraBuilt = {
type: UGOIRA_MEDIA_FILETYPE,
externalUrl: ugoiraMeta.body.originalSrc,
original: ugoiraMeta.body.originalSrc,
otherSources: { zip: ugoiraMeta.body.originalSrc },
filetype: UGOIRA_FILE_EXTENSION,
filename: outputFilepath,
fileCallback: () => {
unlink(outputFilepath).catch(() => {});
},
};
return Promise.resolve(ugoiraBuilt);
});
})
.catch((e) => LogMessageOrError('UgoiraBuilder error:', e));
export default UgoiraBuilder;