mirror of
https://github.com/diced/zipline.git
synced 2025-12-12 07:40:45 -08:00
feat: thumbnails output format (jpg, png, webp)
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "public"."Zipline" ADD COLUMN "featuresThumbnailsFormat" TEXT NOT NULL DEFAULT 'jpg';
|
||||
@@ -56,6 +56,7 @@ model Zipline {
|
||||
|
||||
featuresThumbnailsEnabled Boolean @default(true)
|
||||
featuresThumbnailsNumberThreads Int @default(4)
|
||||
featuresThumbnailsFormat String @default("jpg")
|
||||
|
||||
featuresMetricsEnabled Boolean @default(true)
|
||||
featuresMetricsAdminOnly Boolean @default(false)
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
LoadingOverlay,
|
||||
NumberInput,
|
||||
Paper,
|
||||
Select,
|
||||
SimpleGrid,
|
||||
Switch,
|
||||
TextInput,
|
||||
@@ -33,6 +34,7 @@ export default function Features({
|
||||
featuresDeleteOnMaxViews: true,
|
||||
featuresThumbnailsEnabled: true,
|
||||
featuresThumbnailsNumberThreads: 4,
|
||||
featuresThumbnailsFormat: 'jpg',
|
||||
featuresMetricsEnabled: true,
|
||||
featuresMetricsAdminOnly: false,
|
||||
featuresMetricsShowUserSpecific: true,
|
||||
@@ -58,6 +60,7 @@ export default function Features({
|
||||
featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews ?? true,
|
||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
||||
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
||||
@@ -82,7 +85,7 @@ export default function Features({
|
||||
|
||||
<Switch
|
||||
label='/robots.txt'
|
||||
description='Enables a robots.txt file for search engine optimization. Requires a server restart.'
|
||||
description='Enables a /robots.txt to stop search crawlers. Requires a server restart.'
|
||||
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||
/>
|
||||
|
||||
@@ -143,6 +146,20 @@ export default function Features({
|
||||
{...form.getInputProps('featuresThumbnailsNumberThreads')}
|
||||
/>
|
||||
|
||||
<Select
|
||||
label='Thumbnails Format'
|
||||
description='The output format for thumbnails. Requires a server restart.'
|
||||
data={[
|
||||
{ value: 'jpg', label: '.jpg' },
|
||||
{ value: 'png', label: '.png' },
|
||||
{ value: 'webp', label: '.webp' },
|
||||
]}
|
||||
allowDeselect={false}
|
||||
{...form.getInputProps('featuresThumbnailsFormat')}
|
||||
/>
|
||||
|
||||
<div />
|
||||
|
||||
<Switch
|
||||
label='Version Checking'
|
||||
description='Enable version checking for the server. This will check for updates and display the status on the sidebar to all users.'
|
||||
|
||||
@@ -42,6 +42,7 @@ export const DATABASE_TO_PROP = {
|
||||
|
||||
featuresThumbnailsEnabled: 'features.thumbnails.enabled',
|
||||
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
||||
featuresThumbnailsFormat: 'features.thumbnails.format',
|
||||
|
||||
featuresMetricsEnabled: 'features.metrics.enabled',
|
||||
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
||||
|
||||
@@ -68,11 +68,15 @@ export const ENVS = [
|
||||
env('features.userRegistration', 'FEATURES_USER_REGISTRATION', 'boolean', true),
|
||||
env('features.oauthRegistration', 'FEATURES_OAUTH_REGISTRATION', 'boolean', true),
|
||||
env('features.deleteOnMaxViews', 'FEATURES_DELETE_ON_MAX_VIEWS', 'boolean', true),
|
||||
|
||||
env('features.thumbnails.enabled', 'FEATURES_THUMBNAILS_ENABLED', 'boolean', true),
|
||||
env('features.thumbnails.num_threads', 'FEATURES_THUMBNAILS_NUM_THREADS', 'number', true),
|
||||
env('features.thumbnails.format', 'FEATURES_THUMBNAILS_FORMAT', 'string', true),
|
||||
|
||||
env('features.metrics.enabled', 'FEATURES_METRICS_ENABLED', 'boolean', true),
|
||||
env('features.metrics.adminOnly', 'FEATURES_METRICS_ADMIN_ONLY', 'boolean', true),
|
||||
env('features.metrics.showUserSpecific', 'FEATURES_METRICS_SHOW_USER_SPECIFIC', 'boolean', true),
|
||||
|
||||
env('features.versionChecking', 'FEATURES_VERSION_CHECKING', 'boolean', true),
|
||||
env('features.versionAPI', 'FEATURES_VERSION_API', 'string', true),
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ export const rawConfig: any = {
|
||||
removeGpsMetadata: undefined,
|
||||
randomWordsNumAdjectives: undefined,
|
||||
randomWordsSeparator: undefined,
|
||||
defaultCompressionFormat: undefined,
|
||||
},
|
||||
urls: {
|
||||
route: undefined,
|
||||
@@ -57,6 +58,7 @@ export const rawConfig: any = {
|
||||
thumbnails: {
|
||||
enabled: undefined,
|
||||
num_threads: undefined,
|
||||
format: undefined,
|
||||
},
|
||||
metrics: {
|
||||
enabled: undefined,
|
||||
|
||||
@@ -160,6 +160,7 @@ export const schema = z.object({
|
||||
thumbnails: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
num_threads: z.number().default(4),
|
||||
format: z.enum(['jpg', 'png', 'webp']).default('jpg'),
|
||||
}),
|
||||
metrics: z.object({
|
||||
enabled: z.boolean().default(true),
|
||||
|
||||
@@ -34,13 +34,22 @@ if (!enabled) {
|
||||
|
||||
logger.debug('started thumbnail worker');
|
||||
|
||||
function genThumbnail(file: string, thumbnailTmp: string): Promise<Buffer | undefined> {
|
||||
const formatMimes = {
|
||||
jpg: 'image/jpeg',
|
||||
png: 'image/png',
|
||||
webp: 'image/webp',
|
||||
};
|
||||
|
||||
function name(str: string) {
|
||||
return `${str}.${config.features.thumbnails.format}`;
|
||||
}
|
||||
|
||||
function genThumbnail(input: string, output: string): Promise<Buffer | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
ffmpeg(file)
|
||||
ffmpeg(input)
|
||||
.videoFilters('thumbnail')
|
||||
.frames(1)
|
||||
.format('mjpeg')
|
||||
.output(thumbnailTmp)
|
||||
.output(output)
|
||||
.on('start', (cmd) => {
|
||||
logger.debug('generating thumbnail', { cmd });
|
||||
})
|
||||
@@ -51,7 +60,7 @@ function genThumbnail(file: string, thumbnailTmp: string): Promise<Buffer | unde
|
||||
// the method will return an empty buffer since there is no video stream
|
||||
|
||||
logger.error(
|
||||
`file ${file} does not contain any video stream, it is probably an audio file... ignoring...`,
|
||||
`file ${input} does not contain any video stream, it is probably an audio file... ignoring...`,
|
||||
);
|
||||
resolve(Buffer.alloc(0));
|
||||
}
|
||||
@@ -60,17 +69,17 @@ function genThumbnail(file: string, thumbnailTmp: string): Promise<Buffer | unde
|
||||
reject(err);
|
||||
})
|
||||
.on('end', () => {
|
||||
if (!existsSync(thumbnailTmp)) {
|
||||
logger.error('expected thumbnail file does not exist', { thumbnailTmp });
|
||||
unlinkSync(file);
|
||||
if (!existsSync(output)) {
|
||||
logger.error('expected thumbnail file does not exist', { thumbnailTmp: output });
|
||||
unlinkSync(input);
|
||||
return resolve(undefined);
|
||||
}
|
||||
|
||||
const buffer = readFileSync(thumbnailTmp);
|
||||
const buffer = readFileSync(output);
|
||||
|
||||
unlinkSync(thumbnailTmp);
|
||||
unlinkSync(file);
|
||||
logger.debug('removed temporary files', { file, thumbnail: thumbnailTmp });
|
||||
unlinkSync(output);
|
||||
unlinkSync(input);
|
||||
logger.debug('removed temporary files', { file: input, thumbnail: output });
|
||||
|
||||
resolve(buffer);
|
||||
})
|
||||
@@ -107,17 +116,17 @@ async function generate(config: Config, datasource: Datasource, ids: string[]) {
|
||||
writeStream.on('finish', resolve as any);
|
||||
});
|
||||
|
||||
const thumbnailTmpFile = join(config.core.tempDirectory, `zthumbnail_${file.id}.jpg`);
|
||||
const thumbnailTmpFile = join(config.core.tempDirectory, name(`zthumbnail_${file.id}`));
|
||||
const thumbnail = await genThumbnail(tmpFile, thumbnailTmpFile);
|
||||
if (!thumbnail) return;
|
||||
|
||||
const existing = await datasource.size(`.thumbnail.${file.id}.jpg`);
|
||||
const existing = await datasource.size(name(`.thumbnail.${file.id}`));
|
||||
if (existing || existing === 0) {
|
||||
await datasource.delete(`.thumbnail.${file.id}.jpg`);
|
||||
await datasource.delete(name(`.thumbnail.${file.id}`));
|
||||
}
|
||||
|
||||
await datasource.put(`.thumbnail.${file.id}.jpg`, thumbnail, {
|
||||
mimetype: 'image/jpeg',
|
||||
await datasource.put(name(`.thumbnail.${file.id}`), thumbnail, {
|
||||
mimetype: formatMimes[config.features.thumbnails.format] || 'image/jpeg',
|
||||
});
|
||||
|
||||
const existingThumbnail = await dbProxy<ThumbnailId>('thumbnail.findFirst', {
|
||||
@@ -131,7 +140,7 @@ async function generate(config: Config, datasource: Datasource, ids: string[]) {
|
||||
t = await dbProxy<ThumbnailId>('thumbnail.create', {
|
||||
data: {
|
||||
fileId: file.id,
|
||||
path: `.thumbnail.${file.id}.jpg`,
|
||||
path: name(`.thumbnail.${file.id}`),
|
||||
},
|
||||
});
|
||||
} else {
|
||||
|
||||
@@ -182,6 +182,7 @@ export default fastifyPlugin(
|
||||
cpus().length,
|
||||
'Number of threads must be less than or equal to the number of CPUs: ' + cpus().length,
|
||||
),
|
||||
featuresThumbnailsFormat: z.enum(['jpg', 'png', 'webp']),
|
||||
|
||||
featuresMetricsEnabled: z.boolean(),
|
||||
featuresMetricsAdminOnly: z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user