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)
|
featuresThumbnailsEnabled Boolean @default(true)
|
||||||
featuresThumbnailsNumberThreads Int @default(4)
|
featuresThumbnailsNumberThreads Int @default(4)
|
||||||
|
featuresThumbnailsFormat String @default("jpg")
|
||||||
|
|
||||||
featuresMetricsEnabled Boolean @default(true)
|
featuresMetricsEnabled Boolean @default(true)
|
||||||
featuresMetricsAdminOnly Boolean @default(false)
|
featuresMetricsAdminOnly Boolean @default(false)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
LoadingOverlay,
|
LoadingOverlay,
|
||||||
NumberInput,
|
NumberInput,
|
||||||
Paper,
|
Paper,
|
||||||
|
Select,
|
||||||
SimpleGrid,
|
SimpleGrid,
|
||||||
Switch,
|
Switch,
|
||||||
TextInput,
|
TextInput,
|
||||||
@@ -33,6 +34,7 @@ export default function Features({
|
|||||||
featuresDeleteOnMaxViews: true,
|
featuresDeleteOnMaxViews: true,
|
||||||
featuresThumbnailsEnabled: true,
|
featuresThumbnailsEnabled: true,
|
||||||
featuresThumbnailsNumberThreads: 4,
|
featuresThumbnailsNumberThreads: 4,
|
||||||
|
featuresThumbnailsFormat: 'jpg',
|
||||||
featuresMetricsEnabled: true,
|
featuresMetricsEnabled: true,
|
||||||
featuresMetricsAdminOnly: false,
|
featuresMetricsAdminOnly: false,
|
||||||
featuresMetricsShowUserSpecific: true,
|
featuresMetricsShowUserSpecific: true,
|
||||||
@@ -58,6 +60,7 @@ export default function Features({
|
|||||||
featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews ?? true,
|
featuresDeleteOnMaxViews: data.settings.featuresDeleteOnMaxViews ?? true,
|
||||||
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
featuresThumbnailsEnabled: data.settings.featuresThumbnailsEnabled ?? true,
|
||||||
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
featuresThumbnailsNumberThreads: data.settings.featuresThumbnailsNumberThreads ?? 4,
|
||||||
|
featuresThumbnailsFormat: data.settings.featuresThumbnailsFormat ?? 'jpg',
|
||||||
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
featuresMetricsEnabled: data.settings.featuresMetricsEnabled ?? true,
|
||||||
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
featuresMetricsAdminOnly: data.settings.featuresMetricsAdminOnly ?? false,
|
||||||
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
featuresMetricsShowUserSpecific: data.settings.featuresMetricsShowUserSpecific ?? true,
|
||||||
@@ -82,7 +85,7 @@ export default function Features({
|
|||||||
|
|
||||||
<Switch
|
<Switch
|
||||||
label='/robots.txt'
|
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' })}
|
{...form.getInputProps('featuresRobotsTxt', { type: 'checkbox' })}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -143,6 +146,20 @@ export default function Features({
|
|||||||
{...form.getInputProps('featuresThumbnailsNumberThreads')}
|
{...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
|
<Switch
|
||||||
label='Version Checking'
|
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.'
|
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',
|
featuresThumbnailsEnabled: 'features.thumbnails.enabled',
|
||||||
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
featuresThumbnailsNumberThreads: 'features.thumbnails.num_threads',
|
||||||
|
featuresThumbnailsFormat: 'features.thumbnails.format',
|
||||||
|
|
||||||
featuresMetricsEnabled: 'features.metrics.enabled',
|
featuresMetricsEnabled: 'features.metrics.enabled',
|
||||||
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
featuresMetricsAdminOnly: 'features.metrics.adminOnly',
|
||||||
|
|||||||
@@ -68,11 +68,15 @@ export const ENVS = [
|
|||||||
env('features.userRegistration', 'FEATURES_USER_REGISTRATION', 'boolean', true),
|
env('features.userRegistration', 'FEATURES_USER_REGISTRATION', 'boolean', true),
|
||||||
env('features.oauthRegistration', 'FEATURES_OAUTH_REGISTRATION', 'boolean', true),
|
env('features.oauthRegistration', 'FEATURES_OAUTH_REGISTRATION', 'boolean', true),
|
||||||
env('features.deleteOnMaxViews', 'FEATURES_DELETE_ON_MAX_VIEWS', 'boolean', true),
|
env('features.deleteOnMaxViews', 'FEATURES_DELETE_ON_MAX_VIEWS', 'boolean', true),
|
||||||
|
|
||||||
env('features.thumbnails.enabled', 'FEATURES_THUMBNAILS_ENABLED', '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.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.enabled', 'FEATURES_METRICS_ENABLED', 'boolean', true),
|
||||||
env('features.metrics.adminOnly', 'FEATURES_METRICS_ADMIN_ONLY', '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.metrics.showUserSpecific', 'FEATURES_METRICS_SHOW_USER_SPECIFIC', 'boolean', true),
|
||||||
|
|
||||||
env('features.versionChecking', 'FEATURES_VERSION_CHECKING', 'boolean', true),
|
env('features.versionChecking', 'FEATURES_VERSION_CHECKING', 'boolean', true),
|
||||||
env('features.versionAPI', 'FEATURES_VERSION_API', 'string', true),
|
env('features.versionAPI', 'FEATURES_VERSION_API', 'string', true),
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const rawConfig: any = {
|
|||||||
removeGpsMetadata: undefined,
|
removeGpsMetadata: undefined,
|
||||||
randomWordsNumAdjectives: undefined,
|
randomWordsNumAdjectives: undefined,
|
||||||
randomWordsSeparator: undefined,
|
randomWordsSeparator: undefined,
|
||||||
|
defaultCompressionFormat: undefined,
|
||||||
},
|
},
|
||||||
urls: {
|
urls: {
|
||||||
route: undefined,
|
route: undefined,
|
||||||
@@ -57,6 +58,7 @@ export const rawConfig: any = {
|
|||||||
thumbnails: {
|
thumbnails: {
|
||||||
enabled: undefined,
|
enabled: undefined,
|
||||||
num_threads: undefined,
|
num_threads: undefined,
|
||||||
|
format: undefined,
|
||||||
},
|
},
|
||||||
metrics: {
|
metrics: {
|
||||||
enabled: undefined,
|
enabled: undefined,
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ export const schema = z.object({
|
|||||||
thumbnails: z.object({
|
thumbnails: z.object({
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
num_threads: z.number().default(4),
|
num_threads: z.number().default(4),
|
||||||
|
format: z.enum(['jpg', 'png', 'webp']).default('jpg'),
|
||||||
}),
|
}),
|
||||||
metrics: z.object({
|
metrics: z.object({
|
||||||
enabled: z.boolean().default(true),
|
enabled: z.boolean().default(true),
|
||||||
|
|||||||
@@ -34,13 +34,22 @@ if (!enabled) {
|
|||||||
|
|
||||||
logger.debug('started thumbnail worker');
|
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) => {
|
return new Promise((resolve, reject) => {
|
||||||
ffmpeg(file)
|
ffmpeg(input)
|
||||||
.videoFilters('thumbnail')
|
.videoFilters('thumbnail')
|
||||||
.frames(1)
|
.frames(1)
|
||||||
.format('mjpeg')
|
.output(output)
|
||||||
.output(thumbnailTmp)
|
|
||||||
.on('start', (cmd) => {
|
.on('start', (cmd) => {
|
||||||
logger.debug('generating thumbnail', { 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
|
// the method will return an empty buffer since there is no video stream
|
||||||
|
|
||||||
logger.error(
|
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));
|
resolve(Buffer.alloc(0));
|
||||||
}
|
}
|
||||||
@@ -60,17 +69,17 @@ function genThumbnail(file: string, thumbnailTmp: string): Promise<Buffer | unde
|
|||||||
reject(err);
|
reject(err);
|
||||||
})
|
})
|
||||||
.on('end', () => {
|
.on('end', () => {
|
||||||
if (!existsSync(thumbnailTmp)) {
|
if (!existsSync(output)) {
|
||||||
logger.error('expected thumbnail file does not exist', { thumbnailTmp });
|
logger.error('expected thumbnail file does not exist', { thumbnailTmp: output });
|
||||||
unlinkSync(file);
|
unlinkSync(input);
|
||||||
return resolve(undefined);
|
return resolve(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const buffer = readFileSync(thumbnailTmp);
|
const buffer = readFileSync(output);
|
||||||
|
|
||||||
unlinkSync(thumbnailTmp);
|
unlinkSync(output);
|
||||||
unlinkSync(file);
|
unlinkSync(input);
|
||||||
logger.debug('removed temporary files', { file, thumbnail: thumbnailTmp });
|
logger.debug('removed temporary files', { file: input, thumbnail: output });
|
||||||
|
|
||||||
resolve(buffer);
|
resolve(buffer);
|
||||||
})
|
})
|
||||||
@@ -107,17 +116,17 @@ async function generate(config: Config, datasource: Datasource, ids: string[]) {
|
|||||||
writeStream.on('finish', resolve as any);
|
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);
|
const thumbnail = await genThumbnail(tmpFile, thumbnailTmpFile);
|
||||||
if (!thumbnail) return;
|
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) {
|
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, {
|
await datasource.put(name(`.thumbnail.${file.id}`), thumbnail, {
|
||||||
mimetype: 'image/jpeg',
|
mimetype: formatMimes[config.features.thumbnails.format] || 'image/jpeg',
|
||||||
});
|
});
|
||||||
|
|
||||||
const existingThumbnail = await dbProxy<ThumbnailId>('thumbnail.findFirst', {
|
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', {
|
t = await dbProxy<ThumbnailId>('thumbnail.create', {
|
||||||
data: {
|
data: {
|
||||||
fileId: file.id,
|
fileId: file.id,
|
||||||
path: `.thumbnail.${file.id}.jpg`,
|
path: name(`.thumbnail.${file.id}`),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ export default fastifyPlugin(
|
|||||||
cpus().length,
|
cpus().length,
|
||||||
'Number of threads must be less than or equal to the number of CPUs: ' + 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(),
|
featuresMetricsEnabled: z.boolean(),
|
||||||
featuresMetricsAdminOnly: z.boolean(),
|
featuresMetricsAdminOnly: z.boolean(),
|
||||||
|
|||||||
Reference in New Issue
Block a user