mirror of
https://github.com/immich-app/immich.git
synced 2026-08-02 00:48:08 -07:00
chore(deps): update dependency eslint-plugin-unicorn to v70 - abandoned (#29684)
* chore(deps): update dependency eslint-plugin-unicorn to v70 * fix: linting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
co-authored by
renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Daniel Dietzler
parent
8061a2e5ff
commit
df970da59e
@@ -44,7 +44,7 @@ export class ActivityService extends BaseService {
|
||||
};
|
||||
|
||||
let activity: Activity | undefined;
|
||||
let duplicate = false;
|
||||
let isDuplicate = false;
|
||||
|
||||
if (dto.type === ReactionType.LIKE) {
|
||||
delete dto.comment;
|
||||
@@ -54,7 +54,7 @@ export class ActivityService extends BaseService {
|
||||
assetId: dto.assetId ?? null,
|
||||
isLiked: true,
|
||||
});
|
||||
duplicate = !!activity;
|
||||
isDuplicate = !!activity;
|
||||
}
|
||||
|
||||
if (!activity) {
|
||||
@@ -65,7 +65,7 @@ export class ActivityService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
return { duplicate, value: mapActivity(activity) };
|
||||
return { duplicate: isDuplicate, value: mapActivity(activity) };
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
|
||||
@@ -226,7 +226,7 @@ export class AlbumService extends BaseService {
|
||||
const events: { id: string; recipients: string[] }[] = [];
|
||||
for (const albumId of allowedAlbumIds) {
|
||||
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
|
||||
const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id));
|
||||
const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)];
|
||||
if (notPresentAssetIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Cannot add another owner');
|
||||
}
|
||||
|
||||
const exists = album.albumUsers.find(({ user: { id } }) => id === userId);
|
||||
const exists = album.albumUsers.some(({ user: { id } }) => id === userId);
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ export class AlbumService extends BaseService {
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
return this.findOrFail(id, auth.user.id, { withAssets: true }).then(mapAlbum);
|
||||
return mapAlbum(await this.findOrFail(id, auth.user.id, { withAssets: true }));
|
||||
}
|
||||
|
||||
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
|
||||
|
||||
@@ -22,7 +22,7 @@ export const render = (index: string, meta: OpenGraphTags) => {
|
||||
<meta property="og:description" content="${description}" />
|
||||
${imageUrl ? `<meta property="og:image" content="${imageUrl}" />` : ''}`;
|
||||
|
||||
return index.replace('<!-- metadata:tags -->', tags);
|
||||
return index.replace('<!-- metadata:tags -->', () => tags);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -163,8 +163,8 @@ const assetEntity = Object.freeze({
|
||||
duration: null,
|
||||
files: [] as AssetFile[],
|
||||
exifInfo: {
|
||||
latitude: 49.533_547,
|
||||
longitude: 10.703_075,
|
||||
latitude: 49.533547,
|
||||
longitude: 10.703075,
|
||||
},
|
||||
livePhotoVideoId: null,
|
||||
} as MapAsset);
|
||||
|
||||
@@ -280,15 +280,17 @@ export class AssetService extends BaseService {
|
||||
|
||||
let chunk: Array<{ id: string; isOffline: boolean }> = [];
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map(({ id, isOffline }) => ({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id, deleteOnDisk: !isOffline },
|
||||
})),
|
||||
);
|
||||
chunk = [];
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map(({ id, isOffline }) => ({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id, deleteOnDisk: !isOffline },
|
||||
})),
|
||||
);
|
||||
chunk = [];
|
||||
};
|
||||
|
||||
const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore);
|
||||
|
||||
@@ -653,13 +653,13 @@ describe(AuthService.name, () => {
|
||||
|
||||
describe('getMobileRedirect', () => {
|
||||
it('should pass along the query params', () => {
|
||||
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual(
|
||||
expect(sut.getMobileRedirect('https://immich.app?code=123&state=456')).toEqual(
|
||||
'app.immich:///oauth-callback?code=123&state=456',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work if called without query params', () => {
|
||||
expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:///oauth-callback?');
|
||||
expect(sut.getMobileRedirect('https://immich.app')).toEqual('app.immich:///oauth-callback?');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -979,7 +979,7 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(profile.picture);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
Buffer.from(pictureBytes.buffer),
|
||||
Buffer.from(pictureBytes.buffer, pictureBytes.byteOffset, pictureBytes.byteLength),
|
||||
expect.objectContaining({ format: 'webp', processInvalidImages: false }),
|
||||
expect.stringContaining(`/data/profile/${user.id}/${fileId}.webp`),
|
||||
);
|
||||
|
||||
@@ -65,9 +65,9 @@ export class AuthService extends BaseService {
|
||||
const user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
|
||||
// Always run bcrypt so response time is constant regardless of whether the email
|
||||
// is registered, preventing timing-based user enumeration.
|
||||
const authenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
|
||||
const isAuthenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
|
||||
|
||||
if (!user || !user.password || !authenticated) {
|
||||
if (!user || !user.password || !isAuthenticated) {
|
||||
this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
|
||||
throw new UnauthorizedException('Incorrect email or password');
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export class AuthService extends BaseService {
|
||||
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||
const { password, newPassword } = dto;
|
||||
const user = await this.userRepository.getForChangePassword(auth.user.id);
|
||||
const valid = this.validateSecret(password, user.password);
|
||||
if (!valid) {
|
||||
const isValid = this.validateSecret(password, user.password);
|
||||
if (!isValid) {
|
||||
throw new BadRequestException('Wrong password');
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
getMobileRedirect(url: string) {
|
||||
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
||||
return `${MOBILE_REDIRECT}?${url.split('?', 2)[1] || ''}`;
|
||||
}
|
||||
|
||||
async authorize(dto: OAuthConfigDto) {
|
||||
@@ -625,7 +625,7 @@ export class AuthService extends BaseService {
|
||||
url: string,
|
||||
) {
|
||||
if (mobileOverrideEnabled && mobileRedirectUri) {
|
||||
return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri);
|
||||
return url.replace(/app\.immich:\/+oauth-callback/, () => mobileRedirectUri);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export class BaseService {
|
||||
ctx.workflowRepository,
|
||||
);
|
||||
|
||||
service.logger.setContext(this.name);
|
||||
service.logger.setContext(BaseService.name);
|
||||
|
||||
return service as T;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export class CliService extends BaseService {
|
||||
|
||||
if (!filesSet.has(name) && rowsSet.has(name)) {
|
||||
migrations.push({ name, status: 'deleted' });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +86,9 @@ export class CliService extends BaseService {
|
||||
}
|
||||
|
||||
async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> {
|
||||
const currentState = await this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
.then((state) => state ?? { isMaintenanceMode: false as const });
|
||||
const currentState = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
|
||||
isMaintenanceMode: false as const,
|
||||
};
|
||||
|
||||
if (!currentState.isMaintenanceMode) {
|
||||
return {
|
||||
@@ -114,9 +113,9 @@ export class CliService extends BaseService {
|
||||
username: 'cli-admin',
|
||||
};
|
||||
|
||||
const state = await this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
.then((state) => state ?? { isMaintenanceMode: false as const });
|
||||
const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
|
||||
isMaintenanceMode: false as const,
|
||||
};
|
||||
|
||||
if (state.isMaintenanceMode) {
|
||||
return {
|
||||
@@ -182,11 +181,7 @@ export class CliService extends BaseService {
|
||||
this.userRepository.getFileSamples(),
|
||||
]);
|
||||
|
||||
const paths = [];
|
||||
|
||||
for (const person of people) {
|
||||
paths.push(person.thumbnailPath);
|
||||
}
|
||||
const paths = Array.from(people, (person) => person.thumbnailPath);
|
||||
|
||||
for (const user of users) {
|
||||
paths.push(user.profileImagePath);
|
||||
|
||||
@@ -143,7 +143,7 @@ export class DatabaseBackupService {
|
||||
|
||||
databaseUsername = parsedUrl.username || parsedUrl.searchParams.get('user');
|
||||
|
||||
url = parsedUrl.toString();
|
||||
url = parsedUrl.href;
|
||||
}
|
||||
|
||||
// assume typical values if we can't parse URL or not present
|
||||
@@ -214,6 +214,7 @@ export class DatabaseBackupService {
|
||||
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
|
||||
args,
|
||||
databaseUsername,
|
||||
// eslint-disable-next-line unicorn/prefer-minimal-ternary
|
||||
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
|
||||
databaseVersion,
|
||||
databaseMajorVersion,
|
||||
@@ -228,7 +229,7 @@ export class DatabaseBackupService {
|
||||
|
||||
this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`);
|
||||
|
||||
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`;
|
||||
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ', 1)[0]}.sql.gz`;
|
||||
const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename);
|
||||
const temporaryFilePath = `${backupFilePath}.tmp`;
|
||||
|
||||
@@ -249,6 +250,7 @@ export class DatabaseBackupService {
|
||||
this.logger.error(`Database Backup Failure: ${error}`);
|
||||
await this.storageRepository
|
||||
.unlink(temporaryFilePath)
|
||||
|
||||
.catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`));
|
||||
throw error;
|
||||
}
|
||||
@@ -354,7 +356,7 @@ export class DatabaseBackupService {
|
||||
): Promise<void> {
|
||||
this.logger.debug(`Database Restore Started`);
|
||||
|
||||
let complete = false;
|
||||
let isComplete = false;
|
||||
try {
|
||||
if (!isValidDatabaseBackupName(filename)) {
|
||||
throw new Error('Invalid backup file format!');
|
||||
@@ -399,7 +401,7 @@ export class DatabaseBackupService {
|
||||
});
|
||||
|
||||
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
|
||||
if (complete) {
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -437,7 +439,7 @@ export class DatabaseBackupService {
|
||||
});
|
||||
|
||||
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
|
||||
if (complete) {
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,7 +455,7 @@ export class DatabaseBackupService {
|
||||
this.logger.error(`Database Restore Failure: ${error}`);
|
||||
throw error;
|
||||
} finally {
|
||||
complete = true;
|
||||
isComplete = true;
|
||||
}
|
||||
|
||||
this.logger.log(`Database Restore Success`);
|
||||
@@ -507,7 +509,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
|
||||
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);
|
||||
|
||||
let readingStdin = false;
|
||||
let isReadingStdin = false;
|
||||
let sequenceIdx = 0;
|
||||
|
||||
let linesSent = 0;
|
||||
@@ -532,19 +534,19 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
const source = new PassThrough({
|
||||
transform(chunk, _encoding, callback) {
|
||||
for (const byte of chunk) {
|
||||
if (!readingStdin && byte === 10 && lastByte !== 10) {
|
||||
if (!isReadingStdin && byte === 10 && lastByte !== 10) {
|
||||
linesSent += 1;
|
||||
}
|
||||
|
||||
lastByte = byte;
|
||||
|
||||
const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
|
||||
const sequence = isReadingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
|
||||
if (sequence[sequenceIdx] === byte) {
|
||||
sequenceIdx += 1;
|
||||
|
||||
if (sequence.length === sequenceIdx) {
|
||||
sequenceIdx = 0;
|
||||
readingStdin = !readingStdin;
|
||||
isReadingStdin = !isReadingStdin;
|
||||
}
|
||||
} else {
|
||||
sequenceIdx = 0;
|
||||
@@ -552,6 +554,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
}
|
||||
|
||||
cbDebounced();
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
@@ -633,6 +636,7 @@ function createSqlOwnerTransformStream(databaseUsername: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push(result);
|
||||
callback();
|
||||
},
|
||||
|
||||
@@ -220,15 +220,15 @@ export class DuplicateService extends BaseService {
|
||||
if (idsToTrash.length > 0) {
|
||||
// TODO: this is duplicated with AssetService.deleteAssets
|
||||
const { trash } = await this.getConfig({ withCache: true });
|
||||
const force = !trash.enabled;
|
||||
const isForce = !trash.enabled;
|
||||
|
||||
await this.assetRepository.updateAll(idsToTrash, {
|
||||
deletedAt: new Date(),
|
||||
status: force ? AssetStatus.Deleted : AssetStatus.Trashed,
|
||||
status: isForce ? AssetStatus.Deleted : AssetStatus.Trashed,
|
||||
duplicateId: null,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
|
||||
await this.eventRepository.emit(isForce ? 'AssetDeleteAll' : 'AssetTrashAll', {
|
||||
assetIds: idsToTrash,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
|
||||
@@ -204,10 +204,12 @@ describe(HlsService.name, () => {
|
||||
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
||||
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
||||
if (event === 'HlsSessionRequest') {
|
||||
const { sessionId: id } = rest[0] as { sessionId: string };
|
||||
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
|
||||
if (event !== 'HlsSessionRequest') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sessionId: id } = rest[0] as { sessionId: string };
|
||||
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -312,12 +312,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${items.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map(({ reportId, path }) =>
|
||||
this.storageRepository
|
||||
.stat(path)
|
||||
.then(() => void 0)
|
||||
.catch(() => reportId),
|
||||
),
|
||||
items.map(async ({ reportId, path }) => {
|
||||
try {
|
||||
await this.storageRepository.stat(path);
|
||||
return;
|
||||
} catch {
|
||||
return reportId;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
@@ -383,12 +385,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${items.length} files to check if they are missing.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map((item) =>
|
||||
this.storageRepository
|
||||
.stat(item.path)
|
||||
.then(() => ({ ...item, exists: true }))
|
||||
.catch(() => ({ ...item, exists: false })),
|
||||
),
|
||||
items.map(async (item) => {
|
||||
try {
|
||||
await this.storageRepository.stat(item.path);
|
||||
return { ...item, exists: true };
|
||||
} catch {
|
||||
return { ...item, exists: false };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const outdatedReports = results
|
||||
@@ -420,12 +424,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
paths.map(({ reportId, path }) =>
|
||||
this.storageRepository
|
||||
.stat(path)
|
||||
.then(() => reportId)
|
||||
.catch(() => void 0),
|
||||
),
|
||||
paths.map(async ({ reportId, path }) => {
|
||||
try {
|
||||
await this.storageRepository.stat(path);
|
||||
return reportId;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
|
||||
@@ -162,10 +162,12 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
|
||||
async unwatch(id: string) {
|
||||
if (this.watchers[id]) {
|
||||
await this.watchers[id]();
|
||||
delete this.watchers[id];
|
||||
if (!Object.hasOwn(this.watchers, id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.watchers[id]();
|
||||
delete this.watchers[id];
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppShutdown' })
|
||||
@@ -252,18 +254,22 @@ export class LibraryService extends BaseService {
|
||||
if (!library) {
|
||||
this.logger.debug(`Library ${job.libraryId} not found, skipping file import`);
|
||||
return JobStatus.Failed;
|
||||
} else if (library.deletedAt) {
|
||||
}
|
||||
if (library.deletedAt) {
|
||||
this.logger.debug(`Library ${job.libraryId} is deleted, won't import assets into it`);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
const assetImports: Insertable<AssetTable>[] = [];
|
||||
await Promise.all(
|
||||
job.paths.map((path) =>
|
||||
this.processEntity(path, library.ownerId, job.libraryId)
|
||||
.then((asset) => assetImports.push(asset))
|
||||
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)),
|
||||
),
|
||||
job.paths.map(async (path) => {
|
||||
try {
|
||||
const asset = await this.processEntity(path, library.ownerId, job.libraryId);
|
||||
assetImports.push(asset);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const assetIds = await this.assetRepository.createAll(assetImports);
|
||||
@@ -316,9 +322,9 @@ export class LibraryService extends BaseService {
|
||||
return validation;
|
||||
}
|
||||
|
||||
const access = await this.storageRepository.checkFileExists(importPath, R_OK);
|
||||
const isAccess = await this.storageRepository.checkFileExists(importPath, R_OK);
|
||||
|
||||
if (!access) {
|
||||
if (!isAccess) {
|
||||
validation.message = 'Lacking read permission for folder';
|
||||
return validation;
|
||||
}
|
||||
@@ -369,18 +375,20 @@ export class LibraryService extends BaseService {
|
||||
|
||||
await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() });
|
||||
|
||||
let assetsFound = false;
|
||||
let isAssetsFound = false;
|
||||
let chunk: string[] = [];
|
||||
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
assetsFound = true;
|
||||
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
|
||||
);
|
||||
chunk = [];
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAssetsFound = true;
|
||||
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
|
||||
);
|
||||
chunk = [];
|
||||
};
|
||||
|
||||
this.logger.debug(`Will delete all assets in library ${libraryId}`);
|
||||
@@ -395,7 +403,7 @@ export class LibraryService extends BaseService {
|
||||
|
||||
await queueChunk();
|
||||
|
||||
if (!assetsFound) {
|
||||
if (!isAssetsFound) {
|
||||
this.logger.log(`Deleting library ${libraryId}`);
|
||||
await this.libraryRepository.delete(libraryId);
|
||||
}
|
||||
@@ -520,7 +528,7 @@ export class LibraryService extends BaseService {
|
||||
break;
|
||||
}
|
||||
case AssetSyncResult.CHECK_OFFLINE: {
|
||||
const isInImportPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
|
||||
const isInImportPath = job.importPaths.some((path) => asset.originalPath.startsWith(path));
|
||||
|
||||
if (!isInImportPath) {
|
||||
this.logger.verbose(
|
||||
@@ -742,28 +750,30 @@ export class LibraryService extends BaseService {
|
||||
let count = 0;
|
||||
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
count += chunk.length;
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LibrarySyncAssets,
|
||||
data: {
|
||||
libraryId: library.id,
|
||||
importPaths: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
assetIds: chunk.map((id) => id),
|
||||
progressCounter: count,
|
||||
totalAssets: assetCount,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
|
||||
const completePercentage = ((100 * count) / assetCount).toFixed(1);
|
||||
|
||||
this.logger.log(
|
||||
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
|
||||
);
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
count += chunk.length;
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LibrarySyncAssets,
|
||||
data: {
|
||||
libraryId: library.id,
|
||||
importPaths: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
assetIds: chunk.map((id) => id),
|
||||
progressCounter: count,
|
||||
totalAssets: assetCount,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
|
||||
const completePercentage = ((100 * count) / assetCount).toFixed(1);
|
||||
|
||||
this.logger.log(
|
||||
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
|
||||
);
|
||||
};
|
||||
|
||||
this.logger.log(`Scanning library ${library.id} for assets missing from disk...`);
|
||||
|
||||
@@ -26,6 +26,7 @@ export class MaintenanceService extends BaseService {
|
||||
getMaintenanceMode(): Promise<MaintenanceModeState> {
|
||||
return this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
|
||||
.then((state) => state ?? { isMaintenanceMode: false });
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,11 @@ export class MediaService extends BaseService {
|
||||
jobs = [];
|
||||
};
|
||||
|
||||
const fullsizeEnabled = config.image.fullsize.enabled;
|
||||
for await (const asset of this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled })) {
|
||||
const isFullsizeEnabled = config.image.fullsize.enabled;
|
||||
for await (const asset of this.assetJobRepository.streamForThumbnailJob({
|
||||
force,
|
||||
fullsizeEnabled: isFullsizeEnabled,
|
||||
})) {
|
||||
if (force || !asset.isEdited) {
|
||||
jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } });
|
||||
}
|
||||
@@ -272,13 +275,14 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
private async extractOriginalImage(asset: ThumbnailAsset, image: SystemConfig['image'], useEdits = false) {
|
||||
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||
const generateFullsize =
|
||||
const isExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||
const extracted = isExtractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||
const isGenerateFullsize =
|
||||
((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') &&
|
||||
!mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
||||
useEdits;
|
||||
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||
const isConvertFullsize =
|
||||
isGenerateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||
|
||||
const thumbSource = extracted ? extracted.buffer : asset.originalPath;
|
||||
const { data, info, colorspace } = await this.decodeImage(
|
||||
@@ -286,7 +290,7 @@ export class MediaService extends BaseService {
|
||||
// only specify orientation to extracted images which don't have EXIF orientation data
|
||||
// or it can double rotate the image
|
||||
extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null },
|
||||
convertFullsize ? undefined : image.preview.size,
|
||||
isConvertFullsize ? undefined : image.preview.size,
|
||||
);
|
||||
|
||||
let isTransparent = false;
|
||||
@@ -299,8 +303,8 @@ export class MediaService extends BaseService {
|
||||
data,
|
||||
info,
|
||||
colorspace,
|
||||
convertFullsize,
|
||||
generateFullsize,
|
||||
convertFullsize: isConvertFullsize,
|
||||
generateFullsize: isGenerateFullsize,
|
||||
isTransparent,
|
||||
};
|
||||
}
|
||||
@@ -620,20 +624,20 @@ export class MediaService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
let partialFallbackSuccess = false;
|
||||
let isPartialFallbackSuccess = false;
|
||||
if (ffmpeg.accelDecode) {
|
||||
try {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
|
||||
ffmpeg = { ...ffmpeg, accelDecode: false };
|
||||
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
|
||||
await this.mediaRepository.transcode(input, output, command);
|
||||
partialFallbackSuccess = true;
|
||||
isPartialFallbackSuccess = true;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error occurred during transcoding: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!partialFallbackSuccess) {
|
||||
if (!isPartialFallbackSuccess) {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||
ffmpeg = { ...ffmpeg, accel: TranscodeHardwareAcceleration.Disabled };
|
||||
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
|
||||
@@ -700,9 +704,9 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean {
|
||||
const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
|
||||
const isScalingEnabled = ffmpegConfig.targetResolution !== 'original';
|
||||
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
|
||||
const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
|
||||
const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes;
|
||||
const maxBitrate = this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||
const isLargerThanTargetBitrate = maxBitrate > 0 && stream.bitrate > maxBitrate;
|
||||
|
||||
@@ -757,13 +761,13 @@ export class MediaService extends BaseService {
|
||||
}): boolean {
|
||||
if (colorspace || profileDescription) {
|
||||
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
||||
} else if (bitsPerSample) {
|
||||
}
|
||||
if (bitsPerSample) {
|
||||
// assume sRGB for 8-bit images with no color profile or colorspace metadata
|
||||
return bitsPerSample === 8;
|
||||
} else {
|
||||
// assume sRGB for images with no relevant metadata
|
||||
return true;
|
||||
}
|
||||
// assume sRGB for images with no relevant metadata
|
||||
return true;
|
||||
}
|
||||
|
||||
private parseBitrateToBps(bitrateString: string) {
|
||||
@@ -776,11 +780,11 @@ export class MediaService extends BaseService {
|
||||
|
||||
if (bitrateString.toLowerCase().endsWith('k')) {
|
||||
return bitrateValue * 1000; // Kilobits per second to bits per second
|
||||
} else if (bitrateString.toLowerCase().endsWith('m')) {
|
||||
return bitrateValue * 1_000_000; // Megabits per second to bits per second
|
||||
} else {
|
||||
return bitrateValue;
|
||||
}
|
||||
if (bitrateString.toLowerCase().endsWith('m')) {
|
||||
return bitrateValue * 1_000_000; // Megabits per second to bits per second
|
||||
}
|
||||
return bitrateValue;
|
||||
}
|
||||
|
||||
private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export class MemoryService extends BaseService {
|
||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
const hasSuccess = results.some(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.memoryRepository.update(id, { updatedAt: new Date() });
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export class MemoryService extends BaseService {
|
||||
canAlwaysRemove: Permission.MemoryDelete,
|
||||
});
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
const hasSuccess = results.some(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.memoryRepository.update(id, { id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
@@ -1396,7 +1396,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
boundingBoxX1: Math.floor((0.485_648_148_148_148_2 - 0.2 / 2) * 1000),
|
||||
boundingBoxX1: Math.floor((0.4856481481481482 - 0.2 / 2) * 1000),
|
||||
}),
|
||||
],
|
||||
[],
|
||||
|
||||
@@ -117,9 +117,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
||||
};
|
||||
|
||||
const getLensModel = (exifTags: ImmichTags): string | null => {
|
||||
const lensModel = String(
|
||||
exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '',
|
||||
).trim();
|
||||
const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim();
|
||||
if (lensModel === '----') {
|
||||
return null;
|
||||
}
|
||||
@@ -286,7 +284,7 @@ export class MetadataService extends BaseService {
|
||||
exifImageHeight: validate(height),
|
||||
exifImageWidth: validate(width),
|
||||
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
||||
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
||||
projectionType: exifTags.ProjectionType ? exifTags.ProjectionType.toUpperCase() : null,
|
||||
bitsPerSample: this.getBitsPerSample(exifTags),
|
||||
colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace),
|
||||
|
||||
@@ -295,7 +293,7 @@ export class MetadataService extends BaseService {
|
||||
exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? (exifTags.DeviceManufacturer || null),
|
||||
model:
|
||||
exifTags.Model ?? exifTags.Device?.ModelName ?? exifTags.AndroidModel ?? (exifTags.DeviceModelName || null),
|
||||
fps: video?.frameRate ?? validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||
fps: video?.frameRate ?? validate(Number(exifTags.VideoFrameRate!)),
|
||||
iso: validate(exifTags.ISO) as number,
|
||||
exposureTime: exifTags.ExposureTime ?? null,
|
||||
lensModel: getLensModel(exifTags),
|
||||
@@ -326,7 +324,7 @@ export class MetadataService extends BaseService {
|
||||
: undefined;
|
||||
|
||||
const videoData =
|
||||
format?.formatName && format?.formatLongName && video?.codecName && video?.timeBase
|
||||
format?.formatName && format.formatLongName && video?.codecName && video?.timeBase
|
||||
? {
|
||||
assetId: asset.id,
|
||||
bitrate: video.bitrate,
|
||||
@@ -362,8 +360,8 @@ export class MetadataService extends BaseService {
|
||||
: undefined;
|
||||
|
||||
const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation);
|
||||
const assetWidth = isSidewards ? validate(height) : validate(width);
|
||||
const assetHeight = isSidewards ? validate(width) : validate(height);
|
||||
const assetWidth = validate(isSidewards ? height : width);
|
||||
const assetHeight = validate(isSidewards ? width : height);
|
||||
|
||||
const tasks = new Tasks();
|
||||
|
||||
@@ -445,8 +443,8 @@ export class MetadataService extends BaseService {
|
||||
|
||||
let sidecarPath = null;
|
||||
for (const candidate of this.getSidecarCandidates(asset)) {
|
||||
const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
|
||||
if (!exists) {
|
||||
const isExists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
|
||||
if (!isExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -806,8 +804,8 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
// write extracted motion video to disk, especially if the encoded-video folder has been deleted
|
||||
const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
|
||||
if (!existsOnDisk) {
|
||||
const isExistsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
|
||||
if (!isExistsOnDisk) {
|
||||
this.storageCore.ensureFolders(motionAsset.originalPath);
|
||||
await this.storageRepository.createFile(motionAsset.originalPath, video);
|
||||
this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`);
|
||||
@@ -1087,6 +1085,7 @@ export class MetadataService extends BaseService {
|
||||
|
||||
private getDuration(tags: ImmichTags): number | null {
|
||||
const duration = tags.Duration;
|
||||
// eslint-disable-next-line unicorn/prefer-number-coercion
|
||||
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
|
||||
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export class PersonService extends BaseService {
|
||||
await this.createNewFeaturePhoto([face.person.id]);
|
||||
}
|
||||
|
||||
return await this.findOrFail(personId).then(mapPerson);
|
||||
return mapPerson(await this.findOrFail(personId));
|
||||
}
|
||||
|
||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
@@ -152,7 +152,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
return mapPerson(await this.findOrFail(id));
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
@@ -192,7 +192,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
let faceId: string | undefined;
|
||||
if (assetId) {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] });
|
||||
const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
|
||||
@@ -655,14 +655,12 @@ export class PersonService extends BaseService {
|
||||
topLeft = { x: topLeft.x * scaleFactor, y: topLeft.y * scaleFactor };
|
||||
bottomRight = { x: bottomRight.x * scaleFactor, y: bottomRight.y * scaleFactor };
|
||||
|
||||
const {
|
||||
points: [invertedTopLeft, invertedBottomRight],
|
||||
} = transformPoints(
|
||||
const [invertedTopLeft, invertedBottomRight] = transformPoints(
|
||||
[topLeft, bottomRight],
|
||||
edits,
|
||||
{ width: asset.width, height: asset.height },
|
||||
{ inverse: true },
|
||||
);
|
||||
).points;
|
||||
|
||||
// make sure topLeft is top-left and bottomRight is bottom-right
|
||||
topLeft = {
|
||||
|
||||
@@ -93,10 +93,7 @@ export class QueueService extends BaseService {
|
||||
private updateConcurrency(config: SystemConfig) {
|
||||
this.logger.debug(`Updating queue concurrency settings`);
|
||||
for (const queueName of Object.values(QueueName)) {
|
||||
let concurrency = 1;
|
||||
if (this.isConcurrentQueue(queueName)) {
|
||||
concurrency = config.job[queueName].concurrency;
|
||||
}
|
||||
const concurrency = this.isConcurrentQueue(queueName) ? config.job[queueName].concurrency : 1;
|
||||
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
|
||||
this.jobRepository.setConcurrency(queueName, concurrency);
|
||||
}
|
||||
@@ -161,9 +158,7 @@ export class QueueService extends BaseService {
|
||||
throw new BadRequestException(`The BackgroundTask queue cannot be paused`);
|
||||
}
|
||||
await this.jobRepository.pause(name);
|
||||
}
|
||||
|
||||
if (dto.isPaused === false) {
|
||||
} else if (dto.isPaused === false) {
|
||||
await this.jobRepository.resume(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ServerService extends BaseService {
|
||||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage);
|
||||
serverInfo.diskUsagePercentage = Number(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
@@ -190,8 +190,12 @@ export class ServerService extends BaseService {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
const { licensePublicKey } = this.configRepository.getEnv();
|
||||
const licenseValid = this.cryptoRepository.verifySha256(dto.licenseKey, dto.activationKey, licensePublicKey.server);
|
||||
if (!licenseValid) {
|
||||
const isLicenseValid = this.cryptoRepository.verifySha256(
|
||||
dto.licenseKey,
|
||||
dto.activationKey,
|
||||
licensePublicKey.server,
|
||||
);
|
||||
if (!isLicenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export class SharedLinkService extends BaseService {
|
||||
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.sharedLinkRepository
|
||||
.getAll({ userId: auth.user.id, id, albumId })
|
||||
|
||||
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ export class SharedLinkService extends BaseService {
|
||||
|
||||
const results: AssetIdsResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
const wasRemoved = removedAssetIds.find((id) => id === assetId);
|
||||
const wasRemoved = removedAssetIds.includes(assetId);
|
||||
if (!wasRemoved) {
|
||||
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
|
||||
continue;
|
||||
|
||||
@@ -43,12 +43,12 @@ export class SmartInfoService extends BaseService {
|
||||
|
||||
const modelChange =
|
||||
oldConfig && oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName;
|
||||
const dimSizeChange = dbDimSize !== dimSize;
|
||||
if (!modelChange && !dimSizeChange) {
|
||||
const isDimSizeChange = dbDimSize !== dimSize;
|
||||
if (!modelChange && !isDimSizeChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimSizeChange) {
|
||||
if (isDimSizeChange) {
|
||||
this.logger.log(
|
||||
`Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`,
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ export class StackService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
||||
if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) {
|
||||
throw new BadRequestException('Primary asset must be in the stack');
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ export class StorageTemplateService extends BaseService {
|
||||
@OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration })
|
||||
async handleMigrationSingle({ id }: JobOf<JobName.StorageTemplateMigrationSingle>): Promise<JobStatus> {
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||
if (!storageTemplateEnabled) {
|
||||
const isStorageTemplateEnabled = config.storageTemplate.enabled;
|
||||
if (!isStorageTemplateEnabled) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
@@ -372,8 +372,8 @@ export class StorageTemplateService extends BaseService {
|
||||
let duplicateCount = 0;
|
||||
|
||||
while (true) {
|
||||
const exists = await this.storageRepository.checkFileExists(destination);
|
||||
if (!exists) {
|
||||
const isExists = await this.storageRepository.checkFileExists(destination);
|
||||
if (!isExists) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export class StorageService extends BaseService {
|
||||
const candidates = ['/data', '/usr/src/app/upload'];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const exists = this.storageRepository.existsSync(candidate);
|
||||
if (exists) {
|
||||
const isExists = this.storageRepository.existsSync(candidate);
|
||||
if (isExists) {
|
||||
targets.push(candidate);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class StorageService extends BaseService {
|
||||
flags.mountChecks = {};
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
let isUpdated = false;
|
||||
|
||||
this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`);
|
||||
|
||||
@@ -73,11 +73,11 @@ export class StorageService extends BaseService {
|
||||
|
||||
if (!flags.mountChecks[folder]) {
|
||||
flags.mountChecks[folder] = true;
|
||||
updated = true;
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (isUpdated) {
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.SystemFlags, flags);
|
||||
this.logger.log('Successfully enabled system mount folders checks');
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export class StorageService extends BaseService {
|
||||
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||
try {
|
||||
this.storageRepository.mkdirSync(folderPath);
|
||||
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||
await this.storageRepository.createFile(internalPath, Buffer.from(Date.now().toString()));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
||||
this.logger.warn('Found existing mount file, skipping creation');
|
||||
@@ -180,7 +180,7 @@ export class StorageService extends BaseService {
|
||||
private async verifyWriteAccess(folder: StorageFolder) {
|
||||
const { internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||
try {
|
||||
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||
await this.storageRepository.overwriteFile(internalPath, Buffer.from(Date.now().toString()));
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to write ${internalPath}: ${error}`);
|
||||
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
|
||||
|
||||
@@ -192,7 +192,11 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth),
|
||||
} as const;
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
for (const type of SYNC_TYPES_ORDER) {
|
||||
if (!dto.types.includes(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const handler = handlers[type as keyof typeof handlers];
|
||||
await handler();
|
||||
}
|
||||
@@ -573,16 +577,16 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const creates = this.syncRepository.albumAsset.getCreates({ ...options, ack: createCheckpoint });
|
||||
let first = true;
|
||||
let isFirst = true;
|
||||
for await (const { updateId, ...data } of creates) {
|
||||
if (first) {
|
||||
if (isFirst) {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV2,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
isFirst = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
@@ -644,16 +648,16 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const creates = this.syncRepository.albumAssetExif.getCreates({ ...options, ack: createCheckpoint });
|
||||
let first = true;
|
||||
let isFirst = true;
|
||||
for await (const { updateId, ...data } of creates) {
|
||||
if (first) {
|
||||
if (isFirst) {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetExifUpdateV1,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
isFirst = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data });
|
||||
}
|
||||
|
||||
@@ -108,10 +108,12 @@ export class TagService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
if (!success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -127,10 +129,12 @@ export class TagService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
if (!success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
@@ -34,10 +34,12 @@ export class TelemetryService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'JobSuccess' })
|
||||
onJobSuccess({ job, response }: ArgOf<'JobSuccess'>) {
|
||||
if (response && Object.values(JobStatus).includes(response as JobStatus)) {
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
if (!(response && Object.values(JobStatus).includes(response as JobStatus))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobError' })
|
||||
|
||||
@@ -27,7 +27,7 @@ export class TimelineService extends BaseService {
|
||||
|
||||
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||
const { userId, ...options } = dto;
|
||||
let userIds: string[] | undefined = undefined;
|
||||
let userIds: string[] | undefined;
|
||||
|
||||
if (userId) {
|
||||
userIds = [userId];
|
||||
@@ -52,7 +52,7 @@ export class TimelineService extends BaseService {
|
||||
if (dto.albumId) {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
|
||||
} else {
|
||||
dto.userId = dto.userId || auth.user.id;
|
||||
dto.userId ||= auth.user.id;
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
@@ -74,12 +74,12 @@ export class TimelineService extends BaseService {
|
||||
}
|
||||
|
||||
if (dto.withPartners) {
|
||||
const requestedLocked = dto.visibility === AssetVisibility.Locked;
|
||||
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const requestedTrash = dto.isTrashed === true;
|
||||
const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
|
||||
const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const isRequestedTrash = dto.isTrashed === true;
|
||||
|
||||
if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) {
|
||||
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
|
||||
throw new BadRequestException(
|
||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
||||
);
|
||||
|
||||
@@ -490,18 +490,15 @@ describe(TranscodingService.name, () => {
|
||||
|
||||
describe('FFmpeg seek per segment', () => {
|
||||
const eiffelSeeks = [
|
||||
0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888,
|
||||
12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776,
|
||||
20.052_149_999_999_997, 22.059_372_222_222_223,
|
||||
0, 1.98715, 3.994372222222222, 6.001594444444444, 8.008816666666666, 10.016038888888888, 12.023261111111111,
|
||||
14.030483333333333, 16.037705555555554, 18.044927777777776, 20.052149999999997, 22.059372222222223,
|
||||
];
|
||||
const waterfallSeeks = [
|
||||
0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476,
|
||||
10.040_260_949_298_812,
|
||||
0, 1.994642826321467, 4.006047357065803, 6.0174518878101395, 8.028856418554476, 10.040260949298812,
|
||||
];
|
||||
const trainSeeks = [
|
||||
0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666,
|
||||
9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667,
|
||||
17.991_666_666_666_667, 19.991_666_666_666_667,
|
||||
0, 1.9916666666666667, 3.9916666666666667, 5.991666666666666, 7.991666666666666, 9.991666666666667,
|
||||
11.991666666666667, 13.991666666666667, 15.991666666666667, 17.991666666666667, 19.991666666666667,
|
||||
];
|
||||
const cases = [
|
||||
...eiffelSeeks.map((expected, segmentIndex) => ({
|
||||
|
||||
@@ -54,7 +54,7 @@ export class TranscodingService extends BaseService {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id })));
|
||||
return Promise.all(this.sessions.values().map(({ id }) => this.onSessionEnd({ sessionId: id })));
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask })
|
||||
@@ -139,9 +139,9 @@ export class TranscodingService extends BaseService {
|
||||
session.variantIndex ??= variantIndex;
|
||||
session.startSegment ??= segmentIndex;
|
||||
const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1;
|
||||
const needsRestart =
|
||||
const isNeedsRestart =
|
||||
session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1;
|
||||
if (needsRestart) {
|
||||
if (isNeedsRestart) {
|
||||
this.stopTranscode(session);
|
||||
session.variantIndex = variantIndex;
|
||||
session.startSegment = segmentIndex;
|
||||
@@ -372,7 +372,7 @@ export class TranscodingService extends BaseService {
|
||||
|
||||
private removeInactiveSessions() {
|
||||
const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS;
|
||||
const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff);
|
||||
const inactiveSessions = this.sessions.values().filter((s) => s.lastActivityTime.getTime() < cutoff);
|
||||
return Promise.all(
|
||||
inactiveSessions.map(async (session) => {
|
||||
try {
|
||||
|
||||
@@ -178,19 +178,19 @@ export class UserService extends BaseService {
|
||||
|
||||
const { licensePublicKey } = this.configRepository.getEnv();
|
||||
|
||||
const clientLicenseValid = this.cryptoRepository.verifySha256(
|
||||
const isClientLicenseValid = this.cryptoRepository.verifySha256(
|
||||
license.licenseKey,
|
||||
license.activationKey,
|
||||
licensePublicKey.client,
|
||||
);
|
||||
|
||||
const serverLicenseValid = this.cryptoRepository.verifySha256(
|
||||
const isServerLicenseValid = this.cryptoRepository.verifySha256(
|
||||
license.licenseKey,
|
||||
license.activationKey,
|
||||
licensePublicKey.server,
|
||||
);
|
||||
|
||||
if (!clientLicenseValid && !serverLicenseValid) {
|
||||
if (!isClientLicenseValid && !isServerLicenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
@@ -294,12 +294,12 @@ export class UserService extends BaseService {
|
||||
await this.eventRepository.emit('UserDelete', user);
|
||||
}
|
||||
|
||||
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean {
|
||||
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, delayUntilDeletion: number): boolean {
|
||||
if (!user.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt);
|
||||
return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt);
|
||||
}
|
||||
|
||||
private async findOrFail(id: string, options: UserFindOptions) {
|
||||
|
||||
@@ -17,7 +17,7 @@ const asNotification = (
|
||||
): ReleaseEventV1 => {
|
||||
return {
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion, {
|
||||
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
|
||||
}),
|
||||
checkedAt,
|
||||
@@ -60,8 +60,8 @@ export class VersionService extends BaseService {
|
||||
this.logger.log(`Adding ${current} to upgrade history`);
|
||||
await this.versionRepository.create({ version: current });
|
||||
|
||||
const needsNewMemories = semver.lt(previousVersion, '1.129.0');
|
||||
if (needsNewMemories) {
|
||||
const isNeedsNewMemories = semver.lt(previousVersion, '1.129.0');
|
||||
if (isNeedsNewMemories) {
|
||||
await this.jobRepository.queue({ name: JobName.MemoryGenerate });
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export class VersionService extends BaseService {
|
||||
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
if (
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion, {
|
||||
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
|
||||
})
|
||||
) {
|
||||
|
||||
@@ -93,6 +93,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
for (const pattern of context.allowedHosts) {
|
||||
const regex = new RegExp(pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*'));
|
||||
if (regex.test(hostname)) {
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count
|
||||
const res = await fetch(...args);
|
||||
|
||||
return {
|
||||
@@ -124,8 +125,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
|
||||
const plugins = await this.pluginRepository.getForLoad();
|
||||
for (const { id, name, version, wasmBytes, methods } of plugins) {
|
||||
const method = methods.some(({ hostFunctions }) => !hostFunctions);
|
||||
if (method) {
|
||||
const isMethod = methods.some(({ hostFunctions }) => !hostFunctions);
|
||||
if (isMethod) {
|
||||
const label = `${name}@${version}`;
|
||||
const key = this.getPluginKey({ id, hostFunctions: false });
|
||||
try {
|
||||
@@ -136,8 +137,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const methodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
|
||||
if (methodWithFunction) {
|
||||
const isMethodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
|
||||
if (isMethodWithFunction) {
|
||||
const label = `${name}@${version}/worker`;
|
||||
const key = this.getPluginKey({ id, hostFunctions: true });
|
||||
try {
|
||||
@@ -381,8 +382,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
// TODO infer from steps
|
||||
let type: T | undefined;
|
||||
for (const targetType of Object.values(WorkflowType)) {
|
||||
const missing = workflow.steps.some((step) => !step.types.includes(targetType));
|
||||
if (!missing) {
|
||||
const isMissing = workflow.steps.some((step) => !step.types.includes(targetType));
|
||||
if (!isMissing) {
|
||||
type = targetType as unknown as T;
|
||||
break;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user