diff --git a/backend/operations.ts b/backend/operations.ts new file mode 100644 index 0000000..bae5174 --- /dev/null +++ b/backend/operations.ts @@ -0,0 +1,103 @@ +import fs from 'fs-extra'; +import sharp from 'sharp'; +import crypto from 'crypto'; +import Vibrant from 'node-vibrant'; +import ffmpeg from 'ffmpeg-static'; +import toArray from 'stream-to-array'; +import { exec } from 'child_process'; +import { removeLocation } from '@xoi/gps-metadata-remover'; +import { isProd } from '@tycrek/joint'; +//@ts-ignore +import shell from 'any-shell-escape'; + +type SrcDest = { src: string, dest: string }; + +/** + * Strips GPS EXIF data from a file + */ +export const removeGPS = (file: string): Promise => { + return new Promise((resolve, reject) => + fs.open(file, 'r+') + .then((fd) => removeLocation(file, + // Read function + (size: number, offset: number): Promise => + fs.read(fd, Buffer.alloc(size), 0, size, offset) + .then(({ buffer }) => Promise.resolve(buffer)), + // Write function + (val: string, offset: number, enc: BufferEncoding): Promise => + fs.write(fd, Buffer.alloc(val.length, val, enc), 0, val.length, offset) + .then(() => Promise.resolve()))) + .then(resolve) + .catch(reject)); +} + +/** + * SHA256 file hasher + */ +export const sha256 = (file: string): Promise => new Promise((resolve, reject) => + toArray(fs.createReadStream(file)) + .then((parts: any[]) => Buffer.concat(parts.map((part: any) => Buffer.isBuffer(part) ? part : Buffer.from(part)))) + .then((buf: Buffer) => crypto.createHash('sha256').update(buf).digest('hex')) + .then((hash: string) => resolve(hash)) + .catch(reject)); + +const VIBRANT = { COLOURS: 256, QUALITY: 3 }; +export const vibrant = (file: string, mimetype: string): Promise => new Promise((resolve, reject) => + // todo: random hex colour + mimetype.includes('video') || mimetype.includes('webp') ? `#335599` + : sharp(file).png().toBuffer() + .then((data) => Vibrant.from(data) + .maxColorCount(VIBRANT.COLOURS) + .quality(VIBRANT.QUALITY) + .getPalette()) + .then((palettes) => resolve(palettes[Object.keys(palettes).sort((a, b) => palettes[b]!.population - palettes[a]!.population)[0]]!.hex)) + .catch((err) => reject(err)) +) + +/** + * Thumbnail operations + */ +export class Thumbnail { + + private static readonly THUMBNAIL = { + QUALITY: 75, + WIDTH: 200 * 2, + HEIGHT: 140 * 2, + } + + private static getImageThumbnail({ src, dest }: SrcDest) { + return new Promise((resolve, reject) => + sharp(src) + .resize(this.THUMBNAIL.WIDTH, this.THUMBNAIL.HEIGHT, { kernel: 'cubic' }) + .jpeg({ quality: this.THUMBNAIL.QUALITY }) + .toFile(dest) + .then(resolve) + .catch(reject)); + } + + private static getVideoThumbnail({ src, dest }: SrcDest) { + exec(this.getCommand({ src, dest })) + } + + private static getCommand({ src, dest }: SrcDest) { + return shell([ + ffmpeg, '-y', + '-v', (isProd() ? 'error' : 'debug'), // Log level + '-i', src, // Input file + '-ss', '00:00:01.000', // Timestamp of frame to grab + '-vf', `scale=${this.THUMBNAIL.WIDTH}:${this.THUMBNAIL.HEIGHT}:force_original_aspect_ratio=increase,crop=${this.THUMBNAIL.WIDTH}:${this.THUMBNAIL.HEIGHT}`, // Dimensions of output file + '-frames:v', '1', // Number of frames to grab + dest // Output file + ]); + } + + // old default + /* +export default (file: FileData): Promise => + new Promise((resolve, reject) => + (file.is.video ? getVideoThumbnail : (file.is.image && !file.mimetype.includes('webp')) ? getImageThumbnail : () => Promise.resolve())(file) + .then(() => resolve((file.is.video || file.is.image) ? getNewName(file.randomId) : file.is.audio ? 'views/ass-audio-icon.png' : 'views/ass-file-icon.png')) + .catch(reject)); + + */ +} diff --git a/src/hash.ts b/src/hash.ts deleted file mode 100644 index 4d0b3db..0000000 --- a/src/hash.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { FileData } from './types/definitions'; -import fs from 'fs-extra'; -import crypto from 'crypto'; -import toArray from 'stream-to-array'; -import { log } from './utils'; - -/** - * Generates a SHA1 hash for the provided file - */ -export default (file: FileData): Promise => - new Promise((resolve, reject) => - toArray((fs.createReadStream(file.path))) - .then((parts: any[]) => Buffer.concat(parts.map((part: any) => (Buffer.isBuffer(part) ? part : Buffer.from(part))))) - .then((buf: Buffer) => crypto.createHash('sha1').update(buf).digest('hex')) // skipcq: JS-D003 - .then((hash: string) => log.debug(`Hash for ${file.originalname}`, hash, 'SHA1, hex').callback(() => resolve(hash))) - .catch(reject)); diff --git a/src/nightmare.ts b/src/nightmare.ts deleted file mode 100644 index c0f053d..0000000 --- a/src/nightmare.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * This strips GPS EXIF data from files - */ - -import { removeLocation } from '@xoi/gps-metadata-remover'; -import fs from 'fs-extra'; - -/** - * This strips GPS EXIF data from files using the @xoi/gps-metadata-remover package - * @returns A Promise that resolves to `true` if GPS data was removed, `false` if not - */ -export const removeGPS = (file: string): Promise => { - return new Promise((resolve, reject) => - fs.open(file, 'r+') - .then((fd) => removeLocation(file, - // Read function - (size: number, offset: number): Promise => - fs.read(fd, Buffer.alloc(size), 0, size, offset) - .then(({ buffer }) => Promise.resolve(buffer)), - // Write function - (val: string, offset: number, enc: BufferEncoding): Promise => - fs.write(fd, Buffer.alloc(val.length, val, enc), 0, val.length, offset) - .then(() => Promise.resolve()))) - .then(resolve) - .catch(reject)); -} diff --git a/src/thumbnails.ts b/src/thumbnails.ts deleted file mode 100644 index 0642279..0000000 --- a/src/thumbnails.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { FileData } from './types/definitions'; -import { Config } from 'ass-json'; -import fs from 'fs-extra'; -import ffmpeg from 'ffmpeg-static'; -import sharp from 'sharp'; - -// @ts-ignore -import shell from 'any-shell-escape'; -import { exec } from 'child_process'; -import { isProd, path } from './utils'; -const { diskFilePath }: Config = fs.readJsonSync(path('config.json')); - -// Thumbnail parameters -const THUMBNAIL = { - QUALITY: 75, - WIDTH: 200 * 2, - HEIGHT: 140 * 2, -} - -/** - * Builds a safe escaped ffmpeg command - */ -function getCommand(src: String, dest: String) { - return shell([ - ffmpeg, '-y', - '-v', (isProd ? 'error' : 'debug'), // Log level - '-i', src, // Input file - '-ss', '00:00:01.000', // Timestamp of frame to grab - '-vf', `scale=${THUMBNAIL.WIDTH}:${THUMBNAIL.HEIGHT}:force_original_aspect_ratio=increase,crop=${THUMBNAIL.WIDTH}:${THUMBNAIL.HEIGHT}`, // Dimensions of output file - '-frames:v', '1', // Number of frames to grab - dest // Output file - ]); -} - -/** - * Builds a thumbnail filename - */ -function getNewName(oldName: String) { - return oldName.concat('.thumbnail.jpg'); -} - -/** - * Builds a path to the thumbnails - */ -function getNewNamePath(oldName: String) { - return path(diskFilePath, 'thumbnails/', getNewName(oldName)); -} - -/** - * Extracts an image from a video file to use as a thumbnail, using ffmpeg - */ -function getVideoThumbnail(file: FileData) { - return new Promise((resolve: Function, reject: Function) => exec( - getCommand(file.path, getNewNamePath(file.randomId)), - // @ts-ignore - (err: Error) => (err ? reject(err) : resolve()) - )); -} - -/** - * Generates a thumbnail for the provided image - */ -function getImageThumbnail(file: FileData) { - return new Promise((resolve, reject) => - sharp(file.path) - .resize(THUMBNAIL.WIDTH, THUMBNAIL.HEIGHT, { kernel: 'cubic' }) - .jpeg({ quality: THUMBNAIL.QUALITY }) - .toFile(getNewNamePath(file.randomId)) - .then(resolve) - .catch(reject)); -} - -/** - * Generates a thumbnail - */ -export default (file: FileData): Promise => - new Promise((resolve, reject) => - (file.is.video ? getVideoThumbnail : (file.is.image && !file.mimetype.includes('webp')) ? getImageThumbnail : () => Promise.resolve())(file) - .then(() => resolve((file.is.video || file.is.image) ? getNewName(file.randomId) : file.is.audio ? 'views/ass-audio-icon.png' : 'views/ass-file-icon.png')) - .catch(reject)); diff --git a/src/vibrant.ts b/src/vibrant.ts deleted file mode 100644 index a0a75a7..0000000 --- a/src/vibrant.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { FileData } from './types/definitions'; -import Vibrant from 'node-vibrant'; -import sharp from 'sharp'; -import { randomHexColour } from './utils'; - -// Vibrant parameters -const COLOR_COUNT = 256; -const QUALITY = 3; - -/** - * Extracts a prominent colour from the provided image file - */ -function getVibrant(file: FileData, resolve: Function, reject: Function) { - sharp(file.path).png().toBuffer() - .then((data) => Vibrant.from(data) - .maxColorCount(COLOR_COUNT) - .quality(QUALITY) - .getPalette()) - .then((palettes) => resolve(palettes[Object.keys(palettes).sort((a, b) => palettes[b]!.population - palettes[a]!.population)[0]]!.hex)) - .catch((err) => reject(err)); -} - -/** - * Extracts a colour from an image file. Returns a random Hex value if provided file is a video - */ -export default (file: FileData): Promise => new Promise((resolve, reject) => (!file.is.image || file.mimetype.includes('webp')) ? resolve(randomHexColour()) : getVibrant(file, resolve, reject)); // skipcq: JS-0229