mirror of
https://github.com/diced/zipline.git
synced 2025-04-11 23:31:17 -05:00
fix: merge #694
* fix: password check before any checking if previewable * chore: add debug logs for raw route and use nodejs' built in pipeline instead of pump * chore: hook request instead of reply for debug request logger * fix: the meta tags return to their natural order, and add a fallback text if the browser can't play the video * chore: narrower typing * feat: gif thumbnails, discord doesn't play it sadly :( * fix: turn gif thumbnails into an opt in feature. might be taxing * fix: nevermind on that narrower typing * fix: prettier :(
This commit is contained in:
parent
df84edd310
commit
478baeca83
9 changed files with 198 additions and 106 deletions
|
@ -125,6 +125,17 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
);
|
||||
};
|
||||
|
||||
if (file.password) {
|
||||
return (
|
||||
<Placeholder
|
||||
Icon={IconFileAlert}
|
||||
text={`This file is password protected. Click to view file (${file.name})`}
|
||||
onClick={() => window.open(file.url)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if ((shouldRenderMarkdown || shouldRenderTex || shouldRenderCode) && !props.overrideRender && popup)
|
||||
return (
|
||||
<>
|
||||
|
@ -143,17 +154,6 @@ export default function Type({ file, popup = false, disableMediaPreview, ...prop
|
|||
return <Placeholder Icon={IconFile} text={`Click to view file (${file.name})`} {...props} />;
|
||||
}
|
||||
|
||||
if (file.password) {
|
||||
return (
|
||||
<Placeholder
|
||||
Icon={IconFileAlert}
|
||||
text={`This file is password protected. Click to view file (${file.name})`}
|
||||
onClick={() => window.open(file.url)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return popup ? (
|
||||
media ? (
|
||||
{
|
||||
|
|
|
@ -119,6 +119,7 @@ export interface ConfigFeatures {
|
|||
robots_txt: string;
|
||||
|
||||
thumbnails: boolean;
|
||||
gif_thumbnails: boolean;
|
||||
}
|
||||
|
||||
export interface ConfigOAuth {
|
||||
|
|
|
@ -164,6 +164,7 @@ export default function readConfig() {
|
|||
map('FEATURES_ROBOTS_TXT', 'boolean', 'features.robots_txt'),
|
||||
|
||||
map('FEATURES_THUMBNAILS', 'boolean', 'features.thumbnails'),
|
||||
map('FEATURES_GIF_THUMBNAILS', 'boolean', 'features.gif_thumbnails'),
|
||||
|
||||
map('CHUNKS_MAX_SIZE', 'human-to-byte', 'chunks.max_size'),
|
||||
map('CHUNKS_CHUNKS_SIZE', 'human-to-byte', 'chunks.chunks_size'),
|
||||
|
|
|
@ -191,6 +191,7 @@ const validator = s.object({
|
|||
default_avatar: s.string.nullable.default(null),
|
||||
robots_txt: s.boolean.default(false),
|
||||
thumbnails: s.boolean.default(false),
|
||||
gif_thumbnails: s.boolean.default(false),
|
||||
})
|
||||
.default({
|
||||
invites: false,
|
||||
|
@ -202,6 +203,7 @@ const validator = s.object({
|
|||
default_avatar: null,
|
||||
robots_txt: false,
|
||||
thumbnails: false,
|
||||
gif_thumbnails: false,
|
||||
}),
|
||||
chunks: s
|
||||
.object({
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import type { File, User, Url } from '@prisma/client';
|
||||
import type { File, Url } from '@prisma/client';
|
||||
import { bytesToHuman } from './bytes';
|
||||
import Logger from 'lib/logger';
|
||||
import type { UserExtended } from 'middleware/withZipline';
|
||||
|
||||
export type ParseValue = {
|
||||
file?: Omit<Partial<File>, 'password'>;
|
||||
url?: Url;
|
||||
user?: User;
|
||||
user?: Partial<UserExtended>;
|
||||
|
||||
link?: string;
|
||||
raw_link?: string;
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function EmbeddedFile({
|
|||
};
|
||||
thumbnail?: Pick<Thumbnail, 'name'>;
|
||||
};
|
||||
user: UserExtended;
|
||||
user: Omit<UserExtended, 'password' | 'secret' | 'totpSecret' | 'ratelimit'>;
|
||||
prismRender: boolean;
|
||||
host: string;
|
||||
mediaType: 'image' | 'video' | 'audio' | 'other';
|
||||
|
@ -35,20 +35,26 @@ export default function EmbeddedFile({
|
|||
const router = useRouter();
|
||||
const {
|
||||
password: provPassword,
|
||||
compress,
|
||||
embed,
|
||||
compress = 'false',
|
||||
embed = 'false',
|
||||
} = router.query as {
|
||||
password?: string;
|
||||
compress?: string;
|
||||
embed?: string;
|
||||
};
|
||||
|
||||
const dataURL = (route: string, useThumb?: boolean, withoutHost?: boolean, pass?: string) =>
|
||||
const dataURL = (
|
||||
route: string,
|
||||
useThumb?: boolean,
|
||||
withoutHost?: boolean,
|
||||
pass?: string,
|
||||
forcedl?: boolean,
|
||||
) =>
|
||||
`${withoutHost ? '' : host}${route}/${encodeURIComponent(
|
||||
(useThumb && !!file.thumbnail && file.thumbnail.name) || file.name,
|
||||
)}?compress=${compress?.toLowerCase() === 'true' || false}${
|
||||
)}${compress.match(/^true/i) ? '?compress=true' : '?compress=false'}${
|
||||
!!pass ? `&password=${encodeURIComponent(pass)}` : ''
|
||||
}`;
|
||||
}${forcedl ? '&download=true' : ''}`;
|
||||
const [opened, setOpened] = useState(file.password);
|
||||
const [password, setPassword] = useState(provPassword || '');
|
||||
const [error, setError] = useState('');
|
||||
|
@ -58,7 +64,7 @@ export default function EmbeddedFile({
|
|||
file.createdAt = new Date(file ? file.createdAt : 0);
|
||||
|
||||
const check = async () => {
|
||||
const res = await fetch(dataURL('/r'));
|
||||
const res = await fetch(dataURL('/r', false, true, password));
|
||||
|
||||
if (res.ok) {
|
||||
setError('');
|
||||
|
@ -73,10 +79,7 @@ export default function EmbeddedFile({
|
|||
const updateMedia: (url?: string) => void = function (url?: string) {
|
||||
if (mediaType === 'other') return;
|
||||
|
||||
const mediaContent = document.getElementById(`${mediaType}_content`) as
|
||||
| HTMLImageElement
|
||||
| HTMLVideoElement
|
||||
| HTMLAudioElement;
|
||||
const mediaContent = document.getElementById(`${mediaType}_content`) as HTMLMediaElement;
|
||||
|
||||
if (document.head.getElementsByClassName('dynamic').length === 0) {
|
||||
const metas: HTMLMetaElement[][] = [];
|
||||
|
@ -150,40 +153,34 @@ export default function EmbeddedFile({
|
|||
return (
|
||||
<>
|
||||
<Head>
|
||||
<meta
|
||||
property='og:url'
|
||||
content={dataURL(router.asPath.replace(('/' + router.query['id']) as string, ''))}
|
||||
/>
|
||||
{!embed && !file.embed && (
|
||||
{!embed.match(/^true/i) && !file.embed && mediaType === 'image' && (
|
||||
<link rel='alternate' type='application/json+oembed' href={`${host}/api/oembed/${file.id}`} />
|
||||
)}
|
||||
{user.embed.title && file.embed && (
|
||||
<meta property='og:title' content={parseString(user.embed.title, { file: file, user })} />
|
||||
<meta property='og:title' content={parseString(user.embed.title, { file, user })} />
|
||||
)}
|
||||
{user.embed.description && file.embed && (
|
||||
<meta
|
||||
property='og:description'
|
||||
content={parseString(user.embed.description, { file: file, user })}
|
||||
/>
|
||||
<meta property='og:description' content={parseString(user.embed.description, { file, user })} />
|
||||
)}
|
||||
{user.embed.siteName && file.embed && (
|
||||
<meta property='og:site_name' content={parseString(user.embed.siteName, { file: file, user })} />
|
||||
<meta property='og:site_name' content={parseString(user.embed.siteName, { file, user })} />
|
||||
)}
|
||||
{user.embed.color && file.embed && (
|
||||
<meta property='theme-color' content={parseString(user.embed.color, { file: file, user })} />
|
||||
<meta property='theme-color' content={parseString(user.embed.color, { file, user })} />
|
||||
)}
|
||||
{embed?.toLowerCase() === 'true' && !file.embed && (
|
||||
{(embed.match(/^true/i) || file.embed) && (
|
||||
<>
|
||||
<meta name='og:title' content={file.name} />
|
||||
<meta property='twitter:title' content={file.name} />
|
||||
{mediaType === 'image' && <meta property='twitter:card' content='summary_large_image' />}
|
||||
{mediaType === 'image' && (
|
||||
<meta name='twitter:image' content={dataURL('/r', false, false, password)} />
|
||||
)}
|
||||
{mediaType === 'image' && <meta property='twitter:card' content='summary_large_image' />}
|
||||
</>
|
||||
)}
|
||||
{mediaType === 'image' && (
|
||||
<>
|
||||
<meta property='og:type' content='image' />
|
||||
<meta property='og:image' itemProp='image' content={dataURL('/r', false, false, password)} />
|
||||
<meta property='og:image:secure_url' content={dataURL('/r', false, false, password)} />
|
||||
<meta property='og:image:alt' content={file.name} />
|
||||
|
@ -193,44 +190,27 @@ export default function EmbeddedFile({
|
|||
{mediaType === 'video' && [
|
||||
...(!!file.thumbnail
|
||||
? [
|
||||
<meta key={1} property='og:image' content={dataURL('/r', true, false, password)} />,
|
||||
<meta
|
||||
property='og:image:url'
|
||||
key='og:image:url'
|
||||
content={dataURL('/r', true, false, password)}
|
||||
/>,
|
||||
<meta
|
||||
key={2}
|
||||
property='og:image:secure_url'
|
||||
key='og:image:secure_url'
|
||||
content={dataURL('/r', true)}
|
||||
/>,
|
||||
<meta property='og:image:type' key='og:image:type' content='image/jpeg' />,
|
||||
<meta
|
||||
name='twitter:image'
|
||||
key='twitter:image'
|
||||
content={dataURL('/r', true, false, password)}
|
||||
/>,
|
||||
<meta
|
||||
key={3}
|
||||
property='og:image:type'
|
||||
content={file.thumbnail.name.split('.').pop() === 'jpg' ? 'image/jpg' : 'image/gif'}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
<meta name='twitter:card' key='twitter:card' content='player' />,
|
||||
<meta name='twitter:player' key='twitter:player' content={dataURL('/r', false, false, password)} />,
|
||||
<meta
|
||||
name='twitter:player:stream'
|
||||
key='twitter:player:stream'
|
||||
content={dataURL('/r', false, false, password)}
|
||||
/>,
|
||||
<meta
|
||||
name='twitter:player:stream:content_type'
|
||||
key='twitter:player:stream:content_type'
|
||||
content={file.mimetype}
|
||||
/>,
|
||||
<meta property='og:type' key='og:type' content='video.other' />,
|
||||
<meta property='og:video' key='og:video' content={dataURL('/r', false, false, password)} />,
|
||||
<meta
|
||||
property='og:video:secure_url'
|
||||
key='og:video:secure_url'
|
||||
content={dataURL('/r', false, false, password)}
|
||||
/>,
|
||||
<meta property='og:video:type' key='og:video:type' content={file.mimetype} />,
|
||||
<meta key={4} property='og:type' content='video.other' />,
|
||||
<meta key={5} property='og:video:url' content={dataURL('/r', false, false, password)} />,
|
||||
<meta key={6} property='og:video:secure_url' content={dataURL('/r', false, false, password)} />,
|
||||
<meta key={7} property='og:video:type' content={file.mimetype} />,
|
||||
<meta key={8} name='twitter:card' content='player' />,
|
||||
<meta key={9} name='twitter:player' content={dataURL('/r', false, false, password)} />,
|
||||
<meta key={10} name='twitter:player:stream' content={dataURL('/r', false, false, password)} />,
|
||||
<meta key={11} name='twitter:player:stream:content_type' content={file.mimetype} />,
|
||||
]}
|
||||
{mediaType === 'audio' && (
|
||||
<>
|
||||
|
@ -340,12 +320,16 @@ export default function EmbeddedFile({
|
|||
maxHeight: '100vh',
|
||||
maxWidth: '100vw',
|
||||
}}
|
||||
src={dataURL('/r', false, true, password)}
|
||||
controls
|
||||
autoPlay
|
||||
muted
|
||||
poster={dataURL('/r', true, true, password)}
|
||||
id='video_content'
|
||||
/>
|
||||
>
|
||||
<source src={dataURL('/r', false, true, password)} />
|
||||
<AnchorNext component={Link} href={dataURL('/r', false, true, password, true)}>
|
||||
Can't preview this file. Click here to download it.
|
||||
</AnchorNext>
|
||||
</video>
|
||||
)}
|
||||
|
||||
{mediaType === 'audio' && (
|
||||
|
@ -353,7 +337,7 @@ export default function EmbeddedFile({
|
|||
)}
|
||||
|
||||
{mediaType === 'other' && (
|
||||
<AnchorNext component={Link} href={dataURL('/r', false, true, password)}>
|
||||
<AnchorNext component={Link} href={dataURL('/r', false, true, password, true)}>
|
||||
Can't preview this file. Click here to download it.
|
||||
</AnchorNext>
|
||||
)}
|
||||
|
|
|
@ -3,10 +3,10 @@ import { guess } from 'lib/mimes';
|
|||
import { extname } from 'path';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
import { createBrotliCompress, createDeflate, createGzip } from 'zlib';
|
||||
import pump from 'pump';
|
||||
import { Transform } from 'stream';
|
||||
import { parseRange } from 'lib/utils/range';
|
||||
import type { File, Thumbnail } from '@prisma/client';
|
||||
import { pipeline } from 'stream/promises';
|
||||
|
||||
function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
||||
fastify.decorateReply('rawFile', rawFile);
|
||||
|
@ -18,12 +18,15 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
|||
filename = isThumb ? file.thumbnail?.name : file.name,
|
||||
fileMime = isThumb ? null : file.mimetype;
|
||||
|
||||
const logger = this.server.logger.child('rawRoute');
|
||||
|
||||
const size = await this.server.datasource.size(filename);
|
||||
if (size === null) return this.notFound();
|
||||
|
||||
const mimetype = await guess(extname(filename).slice(1));
|
||||
|
||||
if (this.request.headers.range) {
|
||||
if (this.request.headers.range && !compress?.match(/^true$/i)) {
|
||||
logger.debug('responding raw file with ranged');
|
||||
const [start, end] = parseRange(this.request.headers.range, size);
|
||||
if (start >= size || end >= size) {
|
||||
const buf = await datasource.get(filename);
|
||||
|
@ -61,14 +64,18 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
|||
|
||||
if (
|
||||
this.server.config.core.compression.enabled &&
|
||||
(compress?.match(/^true$/i) || !this.request.headers['X-Zipline-NoCompress']) &&
|
||||
!!this.request.headers['accept-encoding']
|
||||
)
|
||||
if (
|
||||
size > this.server.config.core.compression.threshold &&
|
||||
(fileMime || mimetype).match(/^(image(?!\/(webp))|vfileeo(?!\/(webm))|text)/)
|
||||
)
|
||||
return this.send(useCompress.call(this, data));
|
||||
compress?.match(/^true$/i) &&
|
||||
!this.request.headers['X-Zipline-NoCompress'] &&
|
||||
!!this.request.headers['accept-encoding'] &&
|
||||
size > this.server.config.core.compression.threshold &&
|
||||
(fileMime || mimetype).match(/^(image(?!\/(webp))|video|text)/)
|
||||
) {
|
||||
logger.debug('responding raw file with compressed');
|
||||
this.hijack();
|
||||
return await useCompress.call(this, data);
|
||||
}
|
||||
|
||||
logger.debug('responding raw file with full size');
|
||||
|
||||
return this.type(mimetype || 'application/octet-stream')
|
||||
.headers({
|
||||
|
@ -83,32 +90,33 @@ function rawFileDecorator(fastify: FastifyInstance, _, done) {
|
|||
}
|
||||
}
|
||||
|
||||
function useCompress(this: FastifyReply, data: NodeJS.ReadableStream) {
|
||||
async function useCompress(this: FastifyReply, data: NodeJS.ReadableStream) {
|
||||
let compress: Transform;
|
||||
|
||||
switch ((this.request.headers['accept-encoding'] as string).split(', ')[0]) {
|
||||
case 'gzip':
|
||||
case 'x-gzip':
|
||||
compress = createGzip();
|
||||
this.header('Content-Encoding', 'gzip');
|
||||
this.raw.writeHead(200, { 'Content-Encoding': 'gzip' });
|
||||
break;
|
||||
case 'deflate':
|
||||
compress = createDeflate();
|
||||
this.header('Content-Encoding', 'deflate');
|
||||
this.raw.writeHead(200, { 'Content-Encoding': 'deflate' });
|
||||
break;
|
||||
case 'br':
|
||||
compress = createBrotliCompress();
|
||||
this.header('Content-Encoding', 'br');
|
||||
this.raw.writeHead(200, { 'Content-Encoding': 'br' });
|
||||
break;
|
||||
default:
|
||||
this.server.logger
|
||||
.child('response')
|
||||
.error(`Unsupported encoding: ${this.request.headers['accept-encoding']}}`);
|
||||
.debug(`Unsupported supplied encoding: ${this.request.headers['accept-encoding']}`);
|
||||
this.raw.writeHead(200, {});
|
||||
break;
|
||||
}
|
||||
if (!compress) return data;
|
||||
setTimeout(() => compress.destroy(), 2000);
|
||||
return pump(data, compress, (err) => (err ? this.server.logger.error(err) : null));
|
||||
if (!compress) return await pipeline(data, this.raw);
|
||||
|
||||
return await pipeline(data, compress, this.raw);
|
||||
}
|
||||
|
||||
export default fastifyPlugin(rawFileDecorator, {
|
||||
|
|
|
@ -80,12 +80,12 @@ async function start() {
|
|||
done();
|
||||
});
|
||||
|
||||
server.addHook('onResponse', (req, reply, done) => {
|
||||
server.addHook('onRequest', (req, reply, done) => {
|
||||
if (config.core.logger) {
|
||||
if (req.url.startsWith('/_next')) return done();
|
||||
|
||||
server.logger.child('response').info(`${req.method} ${req.url} -> ${reply.statusCode}`);
|
||||
server.logger.child('response').debug(
|
||||
server.logger.child('request').info(`${req.method} ${req.url} -> ${reply.statusCode}`);
|
||||
server.logger.child('request').debug(
|
||||
JSON.stringify({
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { type File, PrismaClient, type Thumbnail } from '@prisma/client';
|
||||
import { spawn } from 'child_process';
|
||||
import { type ChildProcess, spawn } from 'child_process';
|
||||
import ffmpeg from 'ffmpeg-static';
|
||||
import { createWriteStream } from 'fs';
|
||||
import { rm } from 'fs/promises';
|
||||
|
@ -25,12 +25,58 @@ if (isMainThread) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
async function loadThumbnail(path) {
|
||||
const args = ['-i', path, '-frames:v', '1', '-f', 'mjpeg', 'pipe:1'];
|
||||
async function getDuration(path): Promise<number> {
|
||||
const args = ['-hide_banner', '-nostdin', '-i', path, '-f', 'null', 'pipe:1'];
|
||||
const lengthMatch = new RegExp(/time=(?<time>(\d{2,}:){2}\d{2}\.\d{2})/);
|
||||
|
||||
const child = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
const data: Buffer = await new Promise((resolve, reject) => {
|
||||
const data: string = await new Promise((resolve, reject) => {
|
||||
const buffers: string[] = [];
|
||||
|
||||
child.stderr.on('data', (d) => child.stdout.emit('data', d));
|
||||
|
||||
child.stdout.on('data', (d) => buffers.push(d.toString()));
|
||||
|
||||
child.once('error', (...a) => {
|
||||
console.log(a);
|
||||
|
||||
reject();
|
||||
});
|
||||
child.once('close', (code) => {
|
||||
if (code !== 0) {
|
||||
const msg = buffers.join('').trim().split('\n');
|
||||
|
||||
logger.debug(`cmd: ${ffmpeg} ${args.join(' ')}\n${msg.join('\n')}`);
|
||||
logger.error(`child exited with code ${code}: ${msg[msg.length - 1]}`);
|
||||
|
||||
if (msg[msg.length - 1].includes('does not contain any stream')) {
|
||||
// mismatched mimetype, for example a video/ogg (.ogg) file with no video stream since
|
||||
// for this specific case just set the mimetype to audio/ogg
|
||||
// the method will return an empty buffer since there is no video stream
|
||||
|
||||
logger.error(`file ${path} does not contain any video stream, it is probably an audio file`);
|
||||
resolve('ow');
|
||||
}
|
||||
|
||||
reject(new Error(`child exited with code ${code} ffmpeg output:\n${msg.join('\n')}`));
|
||||
} else {
|
||||
const trimBuffs: string[] = buffers.filter((val) => lengthMatch.exec(val));
|
||||
resolve(trimBuffs[trimBuffs.length - 1].split('\n')[0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const matchLength = lengthMatch.exec(data);
|
||||
if (!matchLength) return 0;
|
||||
|
||||
const timeArr = matchLength.groups.time.split(':');
|
||||
|
||||
return parseFloat(timeArr.reduce((prev, curr) => (parseFloat(prev) * 60 + parseFloat(curr)).toString()));
|
||||
}
|
||||
|
||||
async function handleChild(child: ChildProcess, path: string, args: string[]): Promise<Buffer> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
const buffers = [];
|
||||
const errorBuffers = [];
|
||||
|
||||
|
@ -78,6 +124,47 @@ async function loadThumbnail(path) {
|
|||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadGifThumbnail(path): Promise<Buffer> {
|
||||
if (!config.features.gif_thumbnails) return;
|
||||
const duration = await getDuration(path);
|
||||
|
||||
if (duration <= 5) return;
|
||||
|
||||
let start: number = duration;
|
||||
const re = () => (start = Math.floor(Math.random() * duration * 100) / 100);
|
||||
while (start + 3 >= duration) re();
|
||||
|
||||
const args = [
|
||||
'-i',
|
||||
path,
|
||||
'-ss',
|
||||
start.toString(),
|
||||
'-t',
|
||||
'3',
|
||||
'-vf',
|
||||
'fps=10,scale=320:-1:flags=lanczos,split[s0][s1];[s0]palettegen=stats_mode=diff[p];[s1][p]paletteuse',
|
||||
'-loop',
|
||||
'0',
|
||||
'-f',
|
||||
'gif',
|
||||
'pipe:1',
|
||||
];
|
||||
|
||||
const child = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
const data: Buffer = await handleChild(child, path, args);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
async function loadThumbnail(path): Promise<Buffer> {
|
||||
const args = ['-i', path, '-frames:v', '1', '-f', 'mjpeg', 'pipe:1'];
|
||||
|
||||
const child = spawn(ffmpeg, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
|
||||
const data: Buffer = await handleChild(child, path, args);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@ -86,7 +173,10 @@ async function loadFileTmp(file: File) {
|
|||
const stream = await datasource.get(file.name);
|
||||
|
||||
// pipe to tmp file
|
||||
const tmpFile = join(config.core.temp_directory, `zipline_thumb_${file.id}_${file.id}.tmp`);
|
||||
const tmpFile = join(
|
||||
config.core.temp_directory,
|
||||
`zipline_thumb_${file.id}_${file.mimetype.replace('/', '_')}.tmp`,
|
||||
);
|
||||
const fileWriteStream = createWriteStream(tmpFile);
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
|
@ -105,18 +195,23 @@ async function start() {
|
|||
const file = videos[i];
|
||||
if (!file.mimetype.startsWith('video/')) {
|
||||
logger.info('file is not a video');
|
||||
process.exit(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (file.thumbnail) {
|
||||
logger.info('thumbnail already exists');
|
||||
process.exit(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tmpFile = await loadFileTmp(file);
|
||||
logger.debug(`loaded file to tmp: ${tmpFile}`);
|
||||
const thumbnail = await loadThumbnail(tmpFile);
|
||||
logger.debug(`loaded thumbnail: ${thumbnail.length} bytes mjpeg`);
|
||||
let useStill = false,
|
||||
thumbnail: Buffer = await loadGifThumbnail(tmpFile);
|
||||
if (!thumbnail) {
|
||||
useStill = true;
|
||||
thumbnail = await loadThumbnail(tmpFile);
|
||||
}
|
||||
logger.debug(`loaded thumbnail: ${thumbnail.length} bytes ${useStill ? 'mjpeg' : 'gif'}`);
|
||||
|
||||
if (thumbnail.length === 0 && file.mimetype === 'video/ogg') {
|
||||
logger.info('file might be an audio file, setting mimetype to audio/ogg to avoid future errors');
|
||||
|
@ -141,7 +236,7 @@ async function start() {
|
|||
data: {
|
||||
thumbnail: {
|
||||
create: {
|
||||
name: `.thumb-${file.id}.jpg`,
|
||||
name: `.thumb-${file.id}.${useStill ? 'jpg' : 'gif'}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -150,7 +245,7 @@ async function start() {
|
|||
},
|
||||
});
|
||||
|
||||
await datasource.save(thumb.name, thumbnail, { type: 'image/jpeg' });
|
||||
await datasource.save(thumb.name, thumbnail, { type: useStill ? 'image/jpeg' : 'image/gif' });
|
||||
|
||||
logger.info(`thumbnail saved - ${thumb.name}`);
|
||||
logger.debug(`thumbnail ${JSON.stringify(thumb)}`);
|
||||
|
|
Loading…
Add table
Reference in a new issue