mirror of https://github.com/tycrek/ass
parent
1c0965c510
commit
f4d05bdb33
@ -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<boolean> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.open(file, 'r+')
|
||||
.then((fd) => removeLocation(file,
|
||||
// Read function
|
||||
(size: number, offset: number): Promise<Buffer> =>
|
||||
fs.read(fd, Buffer.alloc(size), 0, size, offset)
|
||||
.then(({ buffer }) => Promise.resolve(buffer)),
|
||||
// Write function
|
||||
(val: string, offset: number, enc: BufferEncoding): Promise<void> =>
|
||||
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<string> => 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<string> => 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<string> =>
|
||||
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));
|
||||
|
||||
*/
|
||||
}
|
@ -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<string> =>
|
||||
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));
|
@ -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<boolean> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
fs.open(file, 'r+')
|
||||
.then((fd) => removeLocation(file,
|
||||
// Read function
|
||||
(size: number, offset: number): Promise<Buffer> =>
|
||||
fs.read(fd, Buffer.alloc(size), 0, size, offset)
|
||||
.then(({ buffer }) => Promise.resolve(buffer)),
|
||||
// Write function
|
||||
(val: string, offset: number, enc: BufferEncoding): Promise<void> =>
|
||||
fs.write(fd, Buffer.alloc(val.length, val, enc), 0, val.length, offset)
|
||||
.then(() => Promise.resolve())))
|
||||
.then(resolve)
|
||||
.catch(reject));
|
||||
}
|
@ -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<string> =>
|
||||
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));
|
@ -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<string> => new Promise((resolve, reject) => (!file.is.image || file.mimetype.includes('webp')) ? resolve(randomHexColour()) : getVibrant(file, resolve, reject)); // skipcq: JS-0229
|
Loading…
Reference in new issue