1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2024-11-03 21:04:03 +08:00

Merge branch 'bpatrik:master' into master
Some checks failed
pigallery2 / pigallery2-Gitea-Actions (push) Failing after 5m28s

This commit is contained in:
xuthus 2024-07-03 17:07:55 +08:00 committed by GitHub
commit 35f76a4d36
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 125 additions and 79 deletions

View File

@ -17,21 +17,13 @@ jobs:
strategy:
matrix:
node-version: [18.x]
services:
mariadb:
image: mariadb:lts
ports:
- 3306
env:
MYSQL_USER: user
MYSQL_PASSWORD: password
MYSQL_DATABASE: pigallery_test
MYSQL_ROOT_PASSWORD: password
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- uses: getong/mariadb-action@v1.11
with:
mysql database: 'pigallery_test'
mysql root password: 'password'
mysql user: 'user'
mysql password: 'password'
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4

View File

@ -35,7 +35,7 @@ VOLUME ["/app/data/config", "/app/data/db", "/app/data/images", "/app/data/tmp"]
RUN ["node", "./src/backend/index", "--expose-gc", "--run-diagnostics", "--config-path=/app/diagnostics-config.json"]
HEALTHCHECK --interval=40s --timeout=30s --retries=3 --start-period=60s \
CMD wget --quiet --tries=1 --no-check-certificate --spider \
http://localhost:80/heartbeat || exit 1
http://127.0.0.1:80/heartbeat || exit 1
# after a extensive job (like video converting), pigallery calls gc, to clean up everthing as fast as possible
# Exec form entrypoint is need otherwise (using shell form) ENV variables are not properly passed down to the app

View File

@ -36,7 +36,7 @@ VOLUME ["/app/data/config", "/app/data/db", "/app/data/images", "/app/data/tmp"]
RUN ["node", "./src/backend/index", "--expose-gc", "--run-diagnostics", "--config-path=/app/diagnostics-config.json"]
HEALTHCHECK --interval=40s --timeout=30s --retries=3 --start-period=60s \
CMD wget --quiet --tries=1 --no-check-certificate --spider \
http://localhost:80/heartbeat || exit 1
http://127.0.0.1:80/heartbeat || exit 1
# after a extensive job (like video converting), pigallery calls gc, to clean up everthing as fast as possible
# Exec form entrypoint is need otherwise (using shell form) ENV variables are not properly passed down to the app

View File

@ -36,7 +36,7 @@ VOLUME ["/app/data/config", "/app/data/db", "/app/data/images", "/app/data/tmp"]
RUN ["node", "./src/backend/index", "--expose-gc", "--run-diagnostics", "--config-path=/app/diagnostics-config.json"]
HEALTHCHECK --interval=40s --timeout=30s --retries=3 --start-period=60s \
CMD wget --quiet --tries=1 --no-check-certificate --spider \
http://localhost:80/heartbeat || exit 1
http://127.0.0.1:80/heartbeat || exit 1
# after a extensive job (like video converting), pigallery calls gc, to clean up everthing as fast as possible
# Exec form entrypoint is need otherwise (using shell form) ENV variables are not properly passed down to the app

View File

@ -36,7 +36,7 @@ VOLUME ["/app/data/config", "/app/data/db", "/app/data/images", "/app/data/tmp"]
RUN ["node", "./src/backend/index", "--expose-gc", "--run-diagnostics", "--config-path=/app/diagnostics-config.json"]
HEALTHCHECK --interval=40s --timeout=30s --retries=3 --start-period=60s \
CMD wget --quiet --tries=1 --no-check-certificate --spider \
http://localhost:80/heartbeat || exit 1
http://127.0.0.1:80/heartbeat || exit 1
# after a extensive job (like video converting), pigallery calls gc, to clean up everthing as fast as possible
# Exec form entrypoint is need otherwise (using shell form) ENV variables are not properly passed down to the app

View File

@ -39,7 +39,7 @@ VOLUME ["/app/data/config", "/app/data/db", "/app/data/images", "/app/data/tmp"]
RUN ["node", "./src/backend/index", "--expose-gc", "--run-diagnostics", "--config-path=/app/diagnostics-config.json"]
HEALTHCHECK --interval=40s --timeout=30s --retries=3 --start-period=60s \
CMD wget --quiet --tries=1 --no-check-certificate --spider \
http://localhost:80/heartbeat || exit 1
http://127.0.0.1:80/heartbeat || exit 1
# after a extensive job (like video converting), pigallery calls gc, to clean up everthing as fast as possible
# Exec form entrypoint is need otherwise (using shell form) ENV variables are not properly passed down to the app

View File

@ -365,7 +365,7 @@ export class SearchManager {
switch (sort.method) {
case SortByTypes.Date:
if (Config.Gallery.ignoreTimestampOffset === true) {
query.addOrderBy('media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)', sort.ascending ? 'ASC' : 'DESC');
query.addOrderBy('media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)', sort.ascending ? 'ASC' : 'DESC');
} else {
query.addOrderBy('media.metadata.creationDate', sort.ascending ? 'ASC' : 'DESC');
}
@ -568,7 +568,7 @@ export class SearchManager {
textParam['from' + queryId] = (query as FromDateSearch).value;
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) ${relation} :from${queryId}`,
`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) ${relation} :from${queryId}`,
textParam
);
} else {
@ -597,7 +597,7 @@ export class SearchManager {
textParam['to' + queryId] = (query as ToDateSearch).value;
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) ${relation} :to${queryId}`,
`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) ${relation} :to${queryId}`,
textParam
);
} else {
@ -809,9 +809,9 @@ export class SearchManager {
if (tq.negate) {
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) >= :to${queryId}`,
`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) >= :to${queryId}`,
textParam
).orWhere(`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) < :from${queryId}`,
).orWhere(`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) < :from${queryId}`,
textParam);
} else {
q.where(
@ -824,9 +824,9 @@ export class SearchManager {
} else {
if (Config.Gallery.ignoreTimestampOffset === true) {
q.where(
`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) < :to${queryId}`,
`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) < :to${queryId}`,
textParam
).andWhere(`(media.metadata.creationDate + (media.metadata.creationDateOffset * 60000)) >= :from${queryId}`,
).andWhere(`(media.metadata.creationDate + (coalesce(media.metadata.creationDateOffset,0) * 60000)) >= :from${queryId}`,
textParam);
} else {
q.where(

View File

@ -10,7 +10,10 @@ export class PersonEntry implements PersonDTO {
@PrimaryGeneratedColumn({unsigned: true})
id: number;
@Column(columnCharsetCS)
@Column({
charset: columnCharsetCS.charset,
collation: columnCharsetCS.collation,
})
name: string;
@Column('int', {unsigned: true, default: 0})

View File

@ -3,12 +3,14 @@ import {Logger} from '../../Logger';
import {NotificationManager} from '../NotifocationManager';
import {SQLConnection} from '../database/SQLConnection';
import * as fs from 'fs';
import * as path from 'path';
import {FFmpegFactory} from '../FFmpegFactory';
import {
ClientAlbumConfig,
ClientFacesConfig,
ClientMapConfig,
ClientMetaFileConfig,
ClientPhotoConfig,
ClientRandomPhotoConfig,
ClientSearchConfig,
ClientSharingConfig,
@ -28,7 +30,9 @@ import {SearchQueryTypes, TextSearch,} from '../../../common/entities/SearchQuer
import {Utils} from '../../../common/Utils';
import {JobRepository} from '../jobs/JobRepository';
import {ConfigClassBuilder} from '../../../../node_modules/typeconfig/node';
import { Config } from '../../../common/config/private/Config';
import {Config} from '../../../common/config/private/Config';
import {SupportedFormats} from '../../../common/SupportedFormats';
import {MediaRendererInput, PhotoWorker, ThumbnailSourceType} from '../fileaccess/PhotoWorker';
const LOG_TAG = '[ConfigDiagnostics]';
@ -78,9 +82,9 @@ export class ConfigDiagnostics {
jobsConfig: ServerJobConfig
): Promise<void> {
Logger.debug(LOG_TAG, 'Testing jobs config');
for(let i = 0; i< jobsConfig.scheduled.length; ++i){
for (let i = 0; i < jobsConfig.scheduled.length; ++i) {
const j = jobsConfig.scheduled[i];
if(!JobRepository.Instance.exists(j.name)){
if (!JobRepository.Instance.exists(j.name)) {
throw new Error('Unknown Job :' + j.name);
}
}
@ -292,6 +296,46 @@ export class ConfigDiagnostics {
}
}
/**
* Removes unsupported image formats.
* It is possible that some OS support one or the other image formats (like Mac os does with HEIC)
* , but others not.
* Those formats are added to the config, but dynamically removed.
* @param config
*/
static async removeUnsupportedPhotoExtensions(config: ClientPhotoConfig): Promise<void> {
Logger.verbose(LOG_TAG, 'Checking for supported image formats');
let removedSome = false;
let i = config.supportedFormats.length;
while (i--) {
const ext = config.supportedFormats[i].toLowerCase();
const testImage = path.join(__dirname, 'image_formats', 'test.' + ext);
// Check if a test available for this image format.
// if not probably because it is trivial
if (!fs.existsSync(testImage)) {
continue;
}
try {
await PhotoWorker.renderFromImage({
type: ThumbnailSourceType.Photo,
mediaPath: testImage,
size: 10,
useLanczos3: Config.Media.Photo.useLanczos3,
quality: Config.Media.Photo.quality,
smartSubsample: Config.Media.Photo.smartSubsample,
} as MediaRendererInput, true
);
} catch (e) {
Logger.verbose(LOG_TAG, 'The current OS does not support the following photo format:' + ext + ', removing it form config.');
config.supportedFormats.splice(i, 1);
removedSome = true;
}
}
if (removedSome) {
SupportedFormats.init();
}
}
static async testConfig(config: PrivateConfigClass): Promise<void> {
await ConfigDiagnostics.testDatabase(config.Database);
@ -310,7 +354,6 @@ export class ConfigDiagnostics {
await ConfigDiagnostics.testRandomPhotoConfig(config.Sharing, config);
await ConfigDiagnostics.testMapConfig(config.Map);
await ConfigDiagnostics.testJobsConfig(config.Jobs);
}
@ -575,6 +618,8 @@ export class ConfigDiagnostics {
Config.Jobs.scheduled = pc.Jobs.scheduled;
}
await this.removeUnsupportedPhotoExtensions(Config.Media.Photo);
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 631 B

View File

@ -23,8 +23,8 @@ export class PhotoWorker {
throw new Error('Unsupported media type to render thumbnail:' + input.type);
}
public static renderFromImage(input: SvgRendererInput | MediaRendererInput): Promise<void> {
return ImageRendererFactory.render(input);
public static renderFromImage(input: SvgRendererInput | MediaRendererInput, dryRun = false): Promise<void> {
return ImageRendererFactory.render(input, dryRun);
}
public static renderFromVideo(input: MediaRendererInput): Promise<void> {
@ -43,8 +43,8 @@ export enum ThumbnailSourceType {
interface RendererInput {
type: ThumbnailSourceType;
size: number;
makeSquare: boolean;
outPath: string;
makeSquare?: boolean;
outPath?: string;
quality: number;
useLanczos3: boolean;
animate: boolean; // animates the output. Used for Gifs
@ -80,14 +80,14 @@ export class VideoRendererFactory {
let width = null;
let height = null;
for (const stream of data.streams) {
if (stream.width) {
if (stream.width && stream.height && !isNaN(stream.width) && !isNaN(stream.height)) {
width = stream.width;
height = stream.height;
break;
}
}
if (!width || !height) {
return reject('[FFmpeg] Can not read video dimension');
if (!width || !height || isNaN(width) || isNaN(height)) {
return reject(`[FFmpeg] Can not read video dimension. Found: ${{width}}x${{height}}`);
}
const command: FfmpegCommand = ffmpeg(input.mediaPath);
const fileName = path.basename(input.outPath);
@ -132,7 +132,7 @@ export class VideoRendererFactory {
export class ImageRendererFactory {
@ExtensionDecorator(e => e.gallery.ImageRenderer.render)
public static async render(input: MediaRendererInput | SvgRendererInput): Promise<void> {
public static async render(input: MediaRendererInput | SvgRendererInput, dryRun = false): Promise<void> {
let image: Sharp;
if ((input as MediaRendererInput).mediaPath) {
@ -174,17 +174,24 @@ export class ImageRendererFactory {
fit: 'cover',
});
}
let processedImg: sharp.Sharp;
if ((input as MediaRendererInput).mediaPath) {
await image.webp({
processedImg = image.webp({
effort: 6,
quality: input.quality,
smartSubsample: (input as MediaRendererInput).smartSubsample
}).toFile(input.outPath);
});
} else {
if ((input as SvgRendererInput).svgString) {
await image.png({effort: 6, quality: input.quality}).toFile(input.outPath);
processedImg = image.png({effort: 6, quality: input.quality});
}
}
// do not save to file
if (dryRun) {
await processedImg.toBuffer();
return;
}
await processedImg.toFile(input.outPath);
}
}

View File

@ -10,44 +10,45 @@ if (typeof window !== 'undefined') {
Config = require('./config/private/Config').Config;
}
export const SupportedFormats = {
Photos: Config.Media.Photo.supportedFormats,
export class SupportedFormats {
static Photos = Config.Media.Photo.supportedFormats;
// Browser supported video formats
// Read more: https://www.w3schools.com/html/html5_video.asp
Videos: Config.Media.Video.supportedFormats,
MetaFiles: Config.MetaFile.supportedFormats,
static Videos = Config.Media.Video.supportedFormats;
static MetaFiles = Config.MetaFile.supportedFormats;
// These formats need to be transcoded (with the build-in ffmpeg support)
TranscodeNeed: {
static TranscodeNeed = {
// based on libvips, all supported formats for sharp: https://github.com/libvips/libvips
Photos: [] as string[],
Videos: Config.Media.Video.supportedFormatsWithTranscoding,
},
};
// --------------------------------------------
// Below this, it is autogenerated, DO NOT EDIT
WithDots: {
static WithDots = {
Photos: [] as string[],
Videos: [] as string[],
MetaFiles: [] as string[],
TranscodeNeed: {
Photos: [] as string[],
Videos: [] as string[],
},
},
};
SupportedFormats.Photos = SupportedFormats.Photos.concat(
}
};
static init(){
SupportedFormats.Photos = SupportedFormats.Photos.concat(
SupportedFormats.TranscodeNeed.Photos
);
SupportedFormats.Videos = SupportedFormats.Videos.concat(
);
SupportedFormats.Videos = SupportedFormats.Videos.concat(
SupportedFormats.TranscodeNeed.Videos
);
SupportedFormats.WithDots.Photos = SupportedFormats.Photos.map((f) => '.' + f);
SupportedFormats.WithDots.Videos = SupportedFormats.Videos.map((f) => '.' + f);
SupportedFormats.WithDots.MetaFiles = SupportedFormats.MetaFiles.map(
);
SupportedFormats.WithDots.Photos = SupportedFormats.Photos.map((f) => '.' + f);
SupportedFormats.WithDots.Videos = SupportedFormats.Videos.map((f) => '.' + f);
SupportedFormats.WithDots.MetaFiles = SupportedFormats.MetaFiles.map(
(f) => '.' + f
);
SupportedFormats.WithDots.TranscodeNeed.Photos =
);
SupportedFormats.WithDots.TranscodeNeed.Photos =
SupportedFormats.TranscodeNeed.Photos.map((f) => '.' + f);
SupportedFormats.WithDots.TranscodeNeed.Videos =
SupportedFormats.WithDots.TranscodeNeed.Videos =
SupportedFormats.TranscodeNeed.Videos.map((f) => '.' + f);
}
}
SupportedFormats.init();

View File

@ -1084,7 +1084,7 @@ export class ClientGalleryConfig {
@ConfigProperty({
tags: {
name: $localize`Ignore timestamp offsets`,
name: $localize`Ignore timestamp offsets (use local time)`,
priority: ConfigPriority.advanced,
},
description: $localize`If enabled, timestamp offsets are ignored, meaning that the local times of pictures are used for searching, sorting and grouping. If disabled, global time is used and pictures with no timestamp are assumed to be in UTC (offset +00:00).`
@ -1266,7 +1266,7 @@ export class ClientPhotoConfig {
},
description: $localize`Photo formats that are supported. Browser needs to support these formats natively. Also sharp (libvips) package should be able to convert these formats.`,
})
supportedFormats: string[] = ['gif', 'jpeg', 'jpg', 'jpe', 'png', 'webp', 'svg'];
supportedFormats: string[] = ['gif', 'jpeg', 'jpg', 'jpe', 'png', 'webp', 'svg', 'avif', 'heic'];
}
@SubConfigClass({tags: {client: true}, softReadonly: true})

View File

@ -199,9 +199,6 @@ export class TestHelper {
} as FaceRegion, {
box: {height: 10, width: 10, left: 104, top: 104},
name: 'Unkle Ben'
} as FaceRegion, {
box: {height: 10, width: 10, left: 105, top: 105},
name: 'Arvíztűrő Tükörfúrógép'
} as FaceRegion, {
box: {height: 10, width: 10, left: 201, top: 201},
name: 'R2-D2'

View File

@ -1387,7 +1387,7 @@ describe('SearchManager', (sqlHelper: DBTestHelper) => {
resultOverflow: false
} as SearchResultDTO));
query = ({value: 6, type: SearchQueryTypes.min_person_count} as MinPersonCountSearch);
query = ({value: 5, type: SearchQueryTypes.min_person_count} as MinPersonCountSearch);
expect(Utils.clone(await sm.search(query)))
.to.deep.equalInAnyOrder(removeDir({
searchQuery: query,

View File

@ -24,7 +24,8 @@ describe('DiskMangerWorker', () => {
ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets');
const dir = await DiskManager.scanDirectory('/');
// should match the number of media (photo/video) files in the assets folder
expect(dir.media.length).to.be.equals(16);
// TODO: make this test less flaky. Every time a new image is added to the folder, it fails.
expect(dir.media.length).to.be.equals(17);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg');