feat: add experimental meaningful preview extraction from videos

For now, it sitll requires gifski. It could be rewritten to use webp instead of gifs, because that is a lot faster, uses less space and we could use ffmpeg for it.
This commit is contained in:
Zephyrrus 2020-07-02 03:42:20 +03:00
parent 42f1a1003a
commit a790d7749e
6 changed files with 177 additions and 32 deletions

17
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,17 @@
{
"discord.enabled": true,
"editor.detectIndentation": false,
"editor.insertSpaces": false,
"files.insertFinalNewline": true,
"editor.formatOnPaste": true,
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"prettier.eslintIntergration": true,
"vetur.validation.template": false,
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"prettier.disableLanguages": ["vue"],
"vetur.format.enable": true,
"files.eol": "\n"
}

View File

@ -45,6 +45,7 @@
"dumper.js": "^1.3.1",
"express": "^4.16.4",
"express-rate-limit": "^3.4.0",
"ffmpeg-generate-video-preview": "^1.0.3",
"file-saver": "^2.0.1",
"flexsearch": "^0.6.22",
"fluent-ffmpeg": "^2.1.2",

View File

@ -1,5 +1,8 @@
require('dotenv').config();
const fs = require('fs');
const path = require('path');
const ThumbUtil = require('./utils/ThumbUtil');
const start = async () => {

View File

@ -2,59 +2,85 @@ const jetpack = require('fs-jetpack');
const path = require('path');
const sharp = require('sharp');
const ffmpeg = require('fluent-ffmpeg');
const generatePreview = require('ffmpeg-generate-video-preview');
const imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
const log = require('./Log');
class ThumbUtil {
static imageExtensions = ['.jpg', '.jpeg', '.gif', '.png', '.webp'];
static videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
static thumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs');
static squareThumbPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square');
static videoPreviewPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'preview');
static generateThumbnails(filename) {
const ext = path.extname(filename).toLowerCase();
const output = `${filename.slice(0, -ext.length)}.png`;
if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output);
if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename);
const previewOutput = `${filename.slice(0, -ext.length)}.gif`;
if (ThumbUtil.imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output);
if (ThumbUtil.videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename, previewOutput);
return null;
}
static async generateThumbnailForImage(filename, output) {
const file = await jetpack.readAsync(
path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename),
'buffer'
);
const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename);
const file = await jetpack.readAsync(filePath, 'buffer');
await sharp(file)
.resize(64, 64)
.toFormat('png')
.toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output));
.toFile(path.join(ThumbUtil.squareThumbPath, output));
await sharp(file)
.resize(225, null)
.toFormat('png')
.toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output));
.toFile(path.join(ThumbUtil.thumbPath, output));
}
static generateThumbnailForVideo(filename) {
ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
static generateThumbnailForVideo(filename, output) {
const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename);
ffmpeg(filePath)
.thumbnail({
timestamps: [0],
filename: '%b.png',
folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'),
folder: ThumbUtil.squareThumbPath,
size: '64x64'
})
.on('error', error => log.error(error.message));
ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
ffmpeg(filePath)
.thumbnail({
timestamps: [0],
filename: '%b.png',
folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'),
folder: ThumbUtil.thumbPath,
size: '150x?'
})
.on('error', error => log.error(error.message));
try {
generatePreview({
input: filePath,
width: 150,
output: path.join(ThumbUtil.videoPreviewPath, output)
});
} catch (e) {
console.error(e);
}
}
static getFileThumbnail(filename) {
if (!filename) return null;
const ext = path.extname(filename).toLowerCase();
if (!imageExtensions.includes(ext) && !videoExtensions.includes(ext)) return null;
if (!ThumbUtil.imageExtensions.includes(ext) && !ThumbUtil.videoExtensions.includes(ext)) return null;
return `${filename.slice(0, -ext.length)}.png`;
}
static async removeThumbs(thumbName) {
await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumbName));
await jetpack.removeAsync(ThumbUtil.squareThumbPath, thumbName);
}
}
module.exports = ThumbUtil;

View File

@ -24,6 +24,8 @@ const ThumbUtil = require('./ThumbUtil');
const blockedExtensions = process.env.BLOCKED_EXTENSIONS.split(',');
class Util {
static uploadPath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER);
static uuid() {
return uuidv4();
}
@ -55,7 +57,7 @@ class Util {
}) + path.extname(name).toLowerCase();
// TODO: Change this to look for the file in the db instead of in the filesystem
const exists = jetpack.exists(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename));
const exists = jetpack.exists(path.join(Util.uploadPath, filename));
if (!exists) return filename;
if (i < 5) return retry(i + 1);
log.error('Couldnt allocate identifier for file');
@ -86,10 +88,7 @@ class Util {
}
static async getFileHash(filename) {
const file = await jetpack.readAsync(
path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename),
'buffer'
);
const file = await jetpack.readAsync(path.join(Util.uploadPath, filename), 'buffer');
if (!file) {
log.error(`There was an error reading the file < ${filename} > for hashing`);
return null;
@ -115,13 +114,9 @@ class Util {
static async deleteFile(filename, deleteFromDB = false) {
const thumbName = ThumbUtil.getFileThumbnail(filename);
try {
await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename));
await jetpack.removeAsync(
path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName)
);
await jetpack.removeAsync(
path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName)
);
await jetpack.removeAsync(path.join(Util.uploadPath, filename));
await ThumbUtil.removeThumbs(thumbName);
if (deleteFromDB) {
await db
.table('files')
@ -205,7 +200,7 @@ class Util {
try {
const zip = new Zip();
for (const file of files) {
zip.addLocalFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, file));
zip.addLocalFile(path.join(Util.uploadPath, file));
}
zip.writeZip(
path.join(

109
yarn.lock
View File

@ -1510,6 +1510,11 @@ append-field@^1.0.0:
resolved "https://registry.yarnpkg.com/append-field/-/append-field-1.0.0.tgz#1e3440e915f0b1203d23748e78edd7b9b5b43e56"
integrity sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=
append-type@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/append-type/-/append-type-1.0.2.tgz#a492f350e81ddcb46b787fc605becf6dd8bccbf6"
integrity sha512-hac740vT/SAbrFBLgLIWZqVT5PUAcGTWS5UkDDhr+OCizZSw90WKw6sWAEgGaYd2viIblggypMXwpjzHXOvAQg==
aproba@^1.0.3, aproba@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a"
@ -1565,6 +1570,11 @@ array-slice@^1.0.0:
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-1.1.0.tgz#e368ea15f89bc7069f7ffb89aec3a6c7d4ac22d4"
integrity sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==
array-to-sentence@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/array-to-sentence/-/array-to-sentence-1.1.0.tgz#c804956dafa53232495b205a9452753a258d39fc"
integrity sha1-yASVba+lMjJJWyBalFJ1OiWNOfw=
array-uniq@1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.2.tgz#5fcc373920775723cfd64d65c64bef53bf9eba6d"
@ -1596,6 +1606,14 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
assert-valid-glob-opts@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/assert-valid-glob-opts/-/assert-valid-glob-opts-1.0.0.tgz#ab9b5438ec5e929f5bb08201819affb1227f730a"
integrity sha512-/mttty5Xh7wE4o7ttKaUpBJl0l04xWe3y6muy1j27gyzSsnceK0AYU9owPtUoL9z8+9hnPxztmuhdFZ7jRoyWw==
dependencies:
glob-option-error "^1.0.0"
validate-glob-opts "^1.0.0"
assert@^1.1.1:
version "1.5.0"
resolved "https://registry.yarnpkg.com/assert/-/assert-1.5.0.tgz#55c109aaf6e0aefdb3dc4b71240c70bf574b18eb"
@ -2479,7 +2497,7 @@ commander@2.17.x:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.17.1.tgz#bd77ab7de6de94205ceacc72f1716d29f20a77bf"
integrity sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==
commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3:
commander@^2.15.1, commander@^2.18.0, commander@^2.19.0, commander@^2.20.0, commander@~2.20.3:
version "2.20.3"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
@ -2758,7 +2776,7 @@ cross-spawn@^5.0.1:
shebang-command "^1.2.0"
which "^1.2.9"
cross-spawn@^6.0.5:
cross-spawn@^6.0.0, cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==
@ -3660,6 +3678,19 @@ evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
md5.js "^1.3.4"
safe-buffer "^5.1.1"
execa@^0.10.0:
version "0.10.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50"
integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw==
dependencies:
cross-spawn "^6.0.0"
get-stream "^3.0.0"
is-stream "^1.1.0"
npm-run-path "^2.0.0"
p-finally "^1.0.0"
signal-exit "^3.0.0"
strip-eof "^1.0.0"
execa@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777"
@ -3851,6 +3882,25 @@ feature-policy@0.3.0:
resolved "https://registry.yarnpkg.com/feature-policy/-/feature-policy-0.3.0.tgz#7430e8e54a40da01156ca30aaec1a381ce536069"
integrity sha512-ZtijOTFN7TzCujt1fnNhfWPFPSHeZkesff9AXZj+UEjYBynWNUIYpC87Ve4wHzyexQsImicLu7WsC2LHq7/xrQ==
ffmpeg-generate-video-preview@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/ffmpeg-generate-video-preview/-/ffmpeg-generate-video-preview-1.0.3.tgz#b0b04dedbdfa7180c00bf6d018563b8966edf499"
integrity sha512-IJIs++4mza7u5ShyA6zjNuX0hkqmbkQhOWZjPwWu4gCpNy9NyJ1F8ADm/pV0JqGAuStSCOHRQGuzizxhoHSptA==
dependencies:
commander "^2.15.1"
execa "^0.10.0"
ffmpeg-probe "^1.0.1"
fluent-ffmpeg "^2.1.2"
rmfr "^2.0.0"
tempy "^0.2.1"
ffmpeg-probe@^1.0.1:
version "1.0.6"
resolved "https://registry.yarnpkg.com/ffmpeg-probe/-/ffmpeg-probe-1.0.6.tgz#4dbb127665ef290fb1b3b51cbecca2d8c7c72406"
integrity sha512-zxH4MYEtrbafVQ5p1doGzHjUmjt3zI4cdKFSNVbBMKy0bTc/KwqYwnGsnQVlyBcXgqBPw2YwGBEWF53MRLh3Sw==
dependencies:
execa "^0.10.0"
figgy-pudding@^3.5.1:
version "3.5.2"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.2.tgz#b4eee8148abb01dcf1d1ac34367d59e12fa61d6e"
@ -4268,6 +4318,11 @@ github-from-package@0.0.0:
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=
glob-option-error@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/glob-option-error/-/glob-option-error-1.0.0.tgz#57cc65def9c7d5c1461baf13129bb5403cff6176"
integrity sha1-V8xl3vnH1cFGG68TEpu1QDz/YXY=
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@ -4834,6 +4889,13 @@ indent-string@^4.0.0:
resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251"
integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==
indexed-filter@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/indexed-filter/-/indexed-filter-1.0.3.tgz#7911439191cac588188464640a8db4f6b324973d"
integrity sha512-oBIzs6EARNMzrLgVg20fK52H19WcRHBiukiiEkw9rnnI//8rinEBMLrYdwEfJ9d4K7bjV1L6nSGft6H/qzHNgQ==
dependencies:
append-type "^1.0.1"
indexes-of@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/indexes-of/-/indexes-of-1.0.1.tgz#f30f716c8e2bd346c7b67d3df3915566a7c05607"
@ -4891,6 +4953,13 @@ inquirer@^6.2.2:
strip-ansi "^5.1.0"
through "^2.3.6"
inspect-with-kind@^1.0.4:
version "1.0.5"
resolved "https://registry.yarnpkg.com/inspect-with-kind/-/inspect-with-kind-1.0.5.tgz#fce151d4ce89722c82ca8e9860bb96f9167c316c"
integrity sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g==
dependencies:
kind-of "^6.0.2"
interpret@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.2.0.tgz#d5061a6224be58e8083985f5014d844359576296"
@ -5152,7 +5221,7 @@ is-path-inside@^1.0.0:
dependencies:
path-is-inside "^1.0.1"
is-plain-obj@^1.0.0:
is-plain-obj@^1.0.0, is-plain-obj@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e"
integrity sha1-caUMhCnfync8kqOQpKA7OfzVHT4=
@ -8325,6 +8394,17 @@ ripemd160@^2.0.0, ripemd160@^2.0.1:
hash-base "^3.0.0"
inherits "^2.0.1"
rmfr@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/rmfr/-/rmfr-2.0.0.tgz#8a42e81332550b3f0019b8fb8ab245bea81b6d1c"
integrity sha512-nQptLCZeyyJfgbpf2x97k5YE8vzDn7bhwx9NlvODdhgbU0mL1ruh71X0HYdRaOEvWC7Cr+SfV0p5p+Ib5yOl7A==
dependencies:
assert-valid-glob-opts "^1.0.0"
glob "^7.1.2"
graceful-fs "^4.1.11"
inspect-with-kind "^1.0.4"
rimraf "^2.6.2"
run-async@^2.2.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.0.tgz#e59054a5b86876cfae07f431d18cbaddc594f1e8"
@ -9191,6 +9271,19 @@ tarn@^1.1.5:
resolved "https://registry.yarnpkg.com/tarn/-/tarn-1.1.5.tgz#7be88622e951738b9fa3fb77477309242cdddc2d"
integrity sha512-PMtJ3HCLAZeedWjJPgGnCvcphbCOMbtZpjKgLq3qM5Qq9aQud+XHrL0WlrlgnTyS8U+jrjGbEXprFcQrxPy52g==
temp-dir@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d"
integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=
tempy@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.2.1.tgz#9038e4dbd1c201b74472214179bc2c6f7776e54c"
integrity sha512-LB83o9bfZGrntdqPuRdanIVCPReam9SOZKW0fOy5I9X3A854GGWi0tjCqoXEk84XIEYBc/x9Hq3EFop/H5wJaw==
dependencies:
temp-dir "^1.0.0"
unique-string "^1.0.0"
term-size@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/term-size/-/term-size-1.2.0.tgz#458b83887f288fc56d6fffbfad262e26638efa69"
@ -9724,6 +9817,16 @@ v8flags@^3.1.2:
dependencies:
homedir-polyfill "^1.0.1"
validate-glob-opts@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/validate-glob-opts/-/validate-glob-opts-1.0.2.tgz#ef9f98977d965537ea4f51fa7d5799e9c6ebca91"
integrity sha512-3PKjRQq/R514lUcG9OEiW0u9f7D4fP09A07kmk1JbNn2tfeQdAHhlT+A4dqERXKu2br2rrxSM3FzagaEeq9w+A==
dependencies:
array-to-sentence "^1.1.0"
indexed-filter "^1.0.0"
inspect-with-kind "^1.0.4"
is-plain-obj "^1.1.0"
validate-npm-package-license@^3.0.1:
version "3.0.4"
resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a"