aboutsummaryrefslogtreecommitdiff
path: root/src/api/utils
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/utils')
-rw-r--r--src/api/utils/Log.js7
-rw-r--r--src/api/utils/ThumbUtil.js108
-rw-r--r--src/api/utils/Util.js128
-rw-r--r--src/api/utils/videoPreview/FragmentPreview.js88
-rw-r--r--src/api/utils/videoPreview/FrameIntervalPreview.js73
5 files changed, 325 insertions, 79 deletions
diff --git a/src/api/utils/Log.js b/src/api/utils/Log.js
index 6753f9e..99d11e4 100644
--- a/src/api/utils/Log.js
+++ b/src/api/utils/Log.js
@@ -22,11 +22,10 @@ class Log {
else console.log(chalk.red(args)); // eslint-disable-line no-console
}
- /*
- static dump(args) {
- dump(args);
+ static debug(args) {
+ if (this.checkIfArrayOrObject(args)) dump(args);
+ else console.log(chalk.gray(args)); // eslint-disable-line no-console
}
- */
static checkIfArrayOrObject(thing) {
if (typeof thing === typeof [] || typeof thing === typeof {}) return true;
diff --git a/src/api/utils/ThumbUtil.js b/src/api/utils/ThumbUtil.js
new file mode 100644
index 0000000..98ba5c0
--- /dev/null
+++ b/src/api/utils/ThumbUtil.js
@@ -0,0 +1,108 @@
+const jetpack = require('fs-jetpack');
+const path = require('path');
+const sharp = require('sharp');
+const ffmpeg = require('fluent-ffmpeg');
+const previewUtil = require('./videoPreview/FragmentPreview');
+
+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`;
+ const previewOutput = `${filename.slice(0, -ext.length)}.webm`;
+
+ // eslint-disable-next-line max-len
+ if (ThumbUtil.imageExtensions.includes(ext)) return ThumbUtil.generateThumbnailForImage(filename, output);
+ // eslint-disable-next-line max-len
+ if (ThumbUtil.videoExtensions.includes(ext)) return ThumbUtil.generateThumbnailForVideo(filename, previewOutput);
+ return null;
+ }
+
+ static async generateThumbnailForImage(filename, output) {
+ 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(ThumbUtil.squareThumbPath, output));
+ await sharp(file)
+ .resize(225, null)
+ .toFormat('png')
+ .toFile(path.join(ThumbUtil.thumbPath, output));
+ }
+
+ static async generateThumbnailForVideo(filename, output) {
+ const filePath = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename);
+
+ ffmpeg(filePath)
+ .thumbnail({
+ timestamps: [0],
+ filename: '%b.png',
+ folder: ThumbUtil.squareThumbPath,
+ size: '64x64',
+ })
+ .on('error', (error) => log.error(error.message));
+
+ ffmpeg(filePath)
+ .thumbnail({
+ timestamps: [0],
+ filename: '%b.png',
+ folder: ThumbUtil.thumbPath,
+ size: '150x?',
+ })
+ .on('error', (error) => log.error(error.message));
+
+ try {
+ await previewUtil({
+ input: filePath,
+ width: 150,
+ output: path.join(ThumbUtil.videoPreviewPath, output),
+ log: log.debug,
+ });
+ } catch (e) {
+ log.error(e);
+ }
+ }
+
+ static getFileThumbnail(filename) {
+ if (!filename) return null;
+ const ext = path.extname(filename).toLowerCase();
+
+ const isImage = ThumbUtil.imageExtensions.includes(ext);
+ const isVideo = ThumbUtil.videoExtensions.includes(ext);
+
+ if (isImage) return { thumb: `${filename.slice(0, -ext.length)}.png` };
+ if (isVideo) {
+ return {
+ thumb: `${filename.slice(0, -ext.length)}.png`,
+ preview: `${filename.slice(0, -ext.length)}.webm`,
+ };
+ }
+
+ return null;
+ }
+
+ static async removeThumbs({ thumb, preview }) {
+ if (thumb) {
+ await jetpack.removeAsync(path.join(ThumbUtil.thumbPath, thumb));
+ await jetpack.removeAsync(path.join(ThumbUtil.squareThumbPath, thumb));
+ }
+ if (preview) {
+ await jetpack.removeAsync(path.join(ThumbUtil.videoPreviewPath, preview));
+ }
+ }
+}
+
+module.exports = ThumbUtil;
diff --git a/src/api/utils/Util.js b/src/api/utils/Util.js
index a4af81e..ee4c748 100644
--- a/src/api/utils/Util.js
+++ b/src/api/utils/Util.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-await-in-loop */
const jetpack = require('fs-jetpack');
const randomstring = require('randomstring');
const path = require('path');
@@ -9,23 +10,23 @@ const db = require('knex')({
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE,
- filename: path.join(__dirname, '..', '..', '..', 'database.sqlite')
+ filename: path.join(__dirname, '../../../database.sqlite'),
},
- useNullAsDefault: process.env.DB_CLIENT === 'sqlite' ? true : false
+ useNullAsDefault: process.env.DB_CLIENT === 'sqlite',
});
const moment = require('moment');
-const log = require('../utils/Log');
const crypto = require('crypto');
-const sharp = require('sharp');
-const ffmpeg = require('fluent-ffmpeg');
const Zip = require('adm-zip');
const uuidv4 = require('uuid/v4');
-const imageExtensions = ['.jpg', '.jpeg', '.bmp', '.gif', '.png', '.webp'];
-const videoExtensions = ['.webm', '.mp4', '.wmv', '.avi', '.mov'];
+const log = require('./Log');
+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();
}
@@ -34,63 +35,17 @@ class Util {
return blockedExtensions.includes(extension);
}
- static generateThumbnails(filename) {
- const ext = path.extname(filename).toLowerCase();
- const output = `${filename.slice(0, -ext.length)}.webp`;
- if (imageExtensions.includes(ext)) return this.generateThumbnailForImage(filename, output);
- if (videoExtensions.includes(ext)) return this.generateThumbnailForVideo(filename);
- return null;
- }
-
- static async generateThumbnailForImage(filename, output) {
- const file = await jetpack.readAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename), 'buffer');
- await sharp(file)
- .resize(64, 64)
- .toFormat('webp')
- .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', output));
- await sharp(file)
- .resize(225, null)
- .toFormat('webp')
- .toFile(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', output));
- }
-
- static generateThumbnailForVideo(filename) {
- ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square'),
- size: '64x64'
- })
- .on('error', error => log.error(error.message));
- ffmpeg(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename))
- .thumbnail({
- timestamps: [0],
- filename: '%b.png',
- folder: path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs'),
- size: '150x?'
- })
- .on('error', error => log.error(error.message));
- }
-
- static getFileThumbnail(filename) {
- if (!filename) return null;
- const ext = path.extname(filename).toLowerCase();
- const extension = imageExtensions.includes(ext) ? 'webp' : videoExtensions.includes(ext) ? 'png' : null;
- if (!extension) return null;
- return `${filename.slice(0, -ext.length)}.${extension}`;
- }
-
static constructFilePublicLink(file) {
/*
TODO: This wont work without a reverse proxy serving both
the site and the API under the same domain. Pls fix.
*/
file.url = `${process.env.DOMAIN}/${file.name}`;
- const thumb = this.getFileThumbnail(file.name);
+ const { thumb, preview } = ThumbUtil.getFileThumbnail(file.name) || {};
if (thumb) {
file.thumb = `${process.env.DOMAIN}/thumbs/${thumb}`;
file.thumbSquare = `${process.env.DOMAIN}/thumbs/square/${thumb}`;
+ file.preview = preview && `${process.env.DOMAIN}/thumbs/preview/${preview}`;
}
return file;
}
@@ -99,11 +54,11 @@ class Util {
const retry = (i = 0) => {
const filename = randomstring.generate({
length: parseInt(process.env.GENERATED_FILENAME_LENGTH, 10),
- capitalization: 'lowercase'
+ capitalization: 'lowercase',
}) + 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');
@@ -116,9 +71,12 @@ class Util {
const retry = async (i = 0) => {
const identifier = randomstring.generate({
length: parseInt(process.env.GENERATED_ALBUM_LENGTH, 10),
- capitalization: 'lowercase'
+ capitalization: 'lowercase',
});
- const exists = await db.table('links').where({ identifier }).first();
+ const exists = await db
+ .table('links')
+ .where({ identifier })
+ .first();
if (!exists) return identifier;
/*
It's funny but if you do i++ the asignment never gets done resulting in an infinite loop
@@ -131,7 +89,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;
@@ -143,7 +101,10 @@ class Util {
}
static generateFileHash(data) {
- const hash = crypto.createHash('md5').update(data).digest('hex');
+ const hash = crypto
+ .createHash('md5')
+ .update(data)
+ .digest('hex');
return hash;
}
@@ -152,18 +113,16 @@ class Util {
}
static async deleteFile(filename, deleteFromDB = false) {
- const thumbName = this.getFileThumbnail(filename);
+ const thumbName = ThumbUtil.getFileThumbnail(filename);
try {
- await jetpack.removeAsync(path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, filename));
- if (thumbName) {
- const thumb = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', thumbName);
- const thumbSquare = path.join(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'thumbs', 'square', thumbName);
- if (await jetpack.existsAsync(thumb)) jetpack.removeAsync(thumb);
- if (await jetpack.existsAsync(thumbSquare)) jetpack.removeAsync(thumbSquare);
- }
+ await jetpack.removeAsync(path.join(Util.uploadPath, filename));
+ await ThumbUtil.removeThumbs(thumbName);
if (deleteFromDB) {
- await db.table('files').where('name', filename).delete();
+ await db
+ .table('files')
+ .where('name', filename)
+ .delete();
}
} catch (error) {
log.error(`There was an error removing the file < ${filename} >`);
@@ -175,10 +134,13 @@ class Util {
try {
const fileAlbums = await db.table('albumsFiles').where({ albumId: id });
for (const fileAlbum of fileAlbums) {
- const file = await db.table('files')
+ const file = await db
+ .table('files')
.where({ id: fileAlbum.fileId })
.first();
+
if (!file) continue;
+
await this.deleteFile(file.name, true);
}
} catch (error) {
@@ -201,7 +163,8 @@ class Util {
try {
const fileTags = await db.table('fileTags').where({ tagId: id });
for (const fileTag of fileTags) {
- const file = await db.table('files')
+ const file = await db
+ .table('files')
.where({ id: fileTag.fileId })
.first();
if (!file) continue;
@@ -231,7 +194,10 @@ class Util {
const id = decoded ? decoded.sub : '';
const iat = decoded ? decoded.iat : '';
- const user = await db.table('users').where({ id }).first();
+ const user = await db
+ .table('users')
+ .where({ id })
+ .first();
if (!user || !user.enabled) return false;
if (iat && iat < moment(user.passwordEditedAt).format('x')) return false;
@@ -243,13 +209,25 @@ 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(__dirname, '..', '..', '..', process.env.UPLOAD_FOLDER, 'zips', `${album.userId}-${album.id}.zip`));
+ zip.writeZip(
+ path.join(
+ __dirname,
+ '..',
+ '..',
+ '..',
+ process.env.UPLOAD_FOLDER,
+ 'zips',
+ `${album.userId}-${album.id}.zip`,
+ ),
+ );
} catch (error) {
log.error(error);
}
}
+
+ static generateThumbnails = ThumbUtil.generateThumbnails;
}
module.exports = Util;
diff --git a/src/api/utils/videoPreview/FragmentPreview.js b/src/api/utils/videoPreview/FragmentPreview.js
new file mode 100644
index 0000000..bf623c1
--- /dev/null
+++ b/src/api/utils/videoPreview/FragmentPreview.js
@@ -0,0 +1,88 @@
+/* eslint-disable no-bitwise */
+const ffmpeg = require('fluent-ffmpeg');
+const probe = require('ffmpeg-probe');
+
+const noop = () => {};
+
+const getRandomInt = (min, max) => {
+ const minInt = Math.ceil(min);
+ const maxInt = Math.floor(max);
+
+ // eslint-disable-next-line no-mixed-operators
+ return Math.floor(Math.random() * (maxInt - minInt + 1) + minInt);
+};
+
+const getStartTime = (vDuration, fDuration, ignoreBeforePercent, ignoreAfterPercent) => {
+ // by subtracting the fragment duration we can be sure that the resulting
+ // start time + fragment duration will be less than the video duration
+ const safeVDuration = vDuration - fDuration;
+
+ // if the fragment duration is longer than the video duration
+ if (safeVDuration <= 0) {
+ return 0;
+ }
+
+ return getRandomInt(ignoreBeforePercent * safeVDuration, ignoreAfterPercent * safeVDuration);
+};
+
+module.exports = async (opts) => {
+ const {
+ log = noop,
+
+ // general output options
+ quality = 2,
+ width,
+ height,
+ input,
+ output,
+
+ fragmentDurationSecond = 3,
+ ignoreBeforePercent = 0.25,
+ ignoreAfterPercent = 0.75,
+ } = opts;
+
+ const info = await probe(input);
+
+ let { duration } = info.format;
+ duration = parseInt(duration, 10);
+
+ const startTime = getStartTime(duration, fragmentDurationSecond, ignoreBeforePercent, ignoreAfterPercent);
+
+ const result = { startTime, duration };
+
+ await new Promise((resolve, reject) => {
+ let scale = null;
+
+ if (width && height) {
+ result.width = width | 0;
+ result.height = height | 0;
+ scale = `scale=${width}:${height}`;
+ } else if (width) {
+ result.width = width | 0;
+ result.height = ((info.height * width) / info.width) | 0;
+ scale = `scale=${width}:-1`;
+ } else if (height) {
+ result.height = height | 0;
+ result.width = ((info.width * height) / info.height) | 0;
+ scale = `scale=-1:${height}`;
+ } else {
+ result.width = info.width;
+ result.height = info.height;
+ }
+
+ return ffmpeg()
+ .input(input)
+ .inputOptions([`-ss ${startTime}`])
+ .outputOptions(['-vsync', 'vfr'])
+ .outputOptions(['-q:v', quality, '-vf', scale])
+ .outputOptions([`-t ${fragmentDurationSecond}`])
+ .noAudio()
+ .output(output)
+ .on('start', (cmd) => log && log({ cmd }))
+ .on('end', resolve)
+ .on('error', reject)
+ .run();
+ });
+
+ return result;
+};
diff --git a/src/api/utils/videoPreview/FrameIntervalPreview.js b/src/api/utils/videoPreview/FrameIntervalPreview.js
new file mode 100644
index 0000000..8c5f1c3
--- /dev/null
+++ b/src/api/utils/videoPreview/FrameIntervalPreview.js
@@ -0,0 +1,73 @@
+/* eslint-disable no-bitwise */
+const ffmpeg = require('fluent-ffmpeg');
+const probe = require('ffmpeg-probe');
+
+const noop = () => {};
+
+module.exports = async (opts) => {
+ const {
+ log = noop,
+
+ // general output options
+ quality = 2,
+ width,
+ height,
+ input,
+ output,
+
+ numFrames,
+ numFramesPercent = 0.05,
+ } = opts;
+
+ const info = await probe(input);
+ // const numFramesTotal = parseInt(info.streams[0].nb_frames, 10);
+ const { avg_frame_rate: avgFrameRate, duration } = info.streams[0];
+ const [frames, time] = avgFrameRate.split('/').map((e) => parseInt(e, 10));
+
+ const numFramesTotal = (frames / time) * duration;
+
+ let numFramesToCapture = numFrames || numFramesPercent * numFramesTotal;
+ numFramesToCapture = Math.max(1, Math.min(numFramesTotal, numFramesToCapture)) | 0;
+ const nthFrame = (numFramesTotal / numFramesToCapture) | 0;
+
+ const result = {
+ output,
+ numFrames: numFramesToCapture,
+ };
+
+ await new Promise((resolve, reject) => {
+ let scale = null;
+
+ if (width && height) {
+ result.width = width | 0;
+ result.height = height | 0;
+ scale = `scale=${width}:${height}`;
+ } else if (width) {
+ result.width = width | 0;
+ result.height = ((info.height * width) / info.width) | 0;
+ scale = `scale=${width}:-1`;
+ } else if (height) {
+ result.height = height | 0;
+ result.width = ((info.width * height) / info.height) | 0;
+ scale = `scale=-1:${height}`;
+ } else {
+ result.width = info.width;
+ result.height = info.height;
+ }
+
+ const filter = [`select=not(mod(n\\,${nthFrame}))`, scale].filter(Boolean).join(',');
+
+ ffmpeg(input)
+ .outputOptions(['-vsync', 'vfr'])
+ .outputOptions(['-q:v', quality, '-vf', filter])
+ .noAudio()
+ .outputFormat('webm')
+ .output(output)
+ .on('start', (cmd) => log && log({ cmd }))
+ .on('end', () => resolve())
+ .on('error', (err) => reject(err))
+ .run();
+ });
+
+ return result;
+};