mirror of
https://github.com/immich-app/immich.git
synced 2025-12-20 01:11:46 +03:00
Compare commits
11 Commits
feat/dynam
...
fix/mainte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9698326702 | ||
|
|
6da85dd12b | ||
|
|
23bd27eb30 | ||
|
|
a2d4439e48 | ||
|
|
00bafb899d | ||
|
|
ec63098020 | ||
|
|
8ca692bfb0 | ||
|
|
125de91c71 | ||
|
|
c9b58f5893 | ||
|
|
640fd7308b | ||
|
|
557a79f747 |
@@ -6,6 +6,8 @@ import 'package:immich_mobile/infrastructure/repositories/local_asset.repository
|
|||||||
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
import 'package:immich_mobile/infrastructure/repositories/remote_asset.repository.dart';
|
||||||
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
|
||||||
|
|
||||||
|
typedef _AssetVideoDimension = ({double? width, double? height, bool isFlipped});
|
||||||
|
|
||||||
class AssetService {
|
class AssetService {
|
||||||
final RemoteAssetRepository _remoteAssetRepository;
|
final RemoteAssetRepository _remoteAssetRepository;
|
||||||
final DriftLocalAssetRepository _localAssetRepository;
|
final DriftLocalAssetRepository _localAssetRepository;
|
||||||
@@ -58,44 +60,48 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<double> getAspectRatio(BaseAsset asset) async {
|
Future<double> getAspectRatio(BaseAsset asset) async {
|
||||||
bool isFlipped;
|
final dimension = asset is LocalAsset
|
||||||
double? width;
|
? await _getLocalAssetDimensions(asset)
|
||||||
double? height;
|
: await _getRemoteAssetDimensions(asset as RemoteAsset);
|
||||||
|
|
||||||
if (asset.hasRemote) {
|
if (dimension.width == null || dimension.height == null || dimension.height == 0) {
|
||||||
final exif = await getExif(asset);
|
return 1.0;
|
||||||
isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
|
||||||
width = asset.width?.toDouble();
|
|
||||||
height = asset.height?.toDouble();
|
|
||||||
} else if (asset is LocalAsset) {
|
|
||||||
isFlipped = CurrentPlatform.isAndroid && (asset.orientation == 90 || asset.orientation == 270);
|
|
||||||
width = asset.width?.toDouble();
|
|
||||||
height = asset.height?.toDouble();
|
|
||||||
} else {
|
|
||||||
isFlipped = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return dimension.isFlipped ? dimension.height! / dimension.width! : dimension.width! / dimension.height!;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<_AssetVideoDimension> _getLocalAssetDimensions(LocalAsset asset) async {
|
||||||
|
double? width = asset.width?.toDouble();
|
||||||
|
double? height = asset.height?.toDouble();
|
||||||
|
int orientation = asset.orientation;
|
||||||
|
|
||||||
if (width == null || height == null) {
|
if (width == null || height == null) {
|
||||||
if (asset.hasRemote) {
|
final fetched = await _localAssetRepository.get(asset.id);
|
||||||
final id = asset is LocalAsset ? asset.remoteId! : (asset as RemoteAsset).id;
|
width = fetched?.width?.toDouble();
|
||||||
final remoteAsset = await _remoteAssetRepository.get(id);
|
height = fetched?.height?.toDouble();
|
||||||
width = remoteAsset?.width?.toDouble();
|
orientation = fetched?.orientation ?? 0;
|
||||||
height = remoteAsset?.height?.toDouble();
|
|
||||||
} else {
|
|
||||||
final id = asset is LocalAsset ? asset.id : (asset as RemoteAsset).localId!;
|
|
||||||
final localAsset = await _localAssetRepository.get(id);
|
|
||||||
width = localAsset?.width?.toDouble();
|
|
||||||
height = localAsset?.height?.toDouble();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final orientedWidth = isFlipped ? height : width;
|
// On Android, local assets need orientation correction for 90°/270° rotations
|
||||||
final orientedHeight = isFlipped ? width : height;
|
// On iOS, the Photos framework pre-corrects dimensions
|
||||||
if (orientedWidth != null && orientedHeight != null && orientedHeight > 0) {
|
final isFlipped = CurrentPlatform.isAndroid && (orientation == 90 || orientation == 270);
|
||||||
return orientedWidth / orientedHeight;
|
return (width: width, height: height, isFlipped: isFlipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
return 1.0;
|
Future<_AssetVideoDimension> _getRemoteAssetDimensions(RemoteAsset asset) async {
|
||||||
|
double? width = asset.width?.toDouble();
|
||||||
|
double? height = asset.height?.toDouble();
|
||||||
|
|
||||||
|
if (width == null || height == null) {
|
||||||
|
final fetched = await _remoteAssetRepository.get(asset.id);
|
||||||
|
width = fetched?.width?.toDouble();
|
||||||
|
height = fetched?.height?.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
final exif = await getExif(asset);
|
||||||
|
final isFlipped = ExifDtoConverter.isOrientationFlipped(exif?.orientation);
|
||||||
|
return (width: width, height: height, isFlipped: isFlipped);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<(String, String)>> getPlaces(String userId) {
|
Future<List<(String, String)>> getPlaces(String userId) {
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import 'package:immich_mobile/extensions/translate_extensions.dart';
|
|||||||
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
|
import 'package:immich_mobile/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map.state.dart';
|
||||||
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
|
import 'package:immich_mobile/presentation/widgets/map/map_utils.dart';
|
||||||
|
import 'package:immich_mobile/providers/routes.provider.dart';
|
||||||
|
import 'package:immich_mobile/routing/router.dart';
|
||||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||||
import 'package:immich_mobile/utils/debounce.dart';
|
import 'package:immich_mobile/utils/debounce.dart';
|
||||||
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
import 'package:immich_mobile/widgets/common/immich_toast.dart';
|
||||||
@@ -114,6 +116,14 @@ class _DriftMapState extends ConsumerState<DriftMap> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the AssetViewer is open, the DriftMap route stays alive in the background.
|
||||||
|
// If we continue to update bounds, the map-scoped timeline service gets recreated and the previous one disposed,
|
||||||
|
// which can invalidate the TimelineService instance that was passed into AssetViewerRoute (causing "loading forever").
|
||||||
|
final currentRoute = ref.read(currentRouteNameProvider);
|
||||||
|
if (currentRoute == AssetViewerRoute.name || currentRoute == GalleryViewerRoute.name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final bounds = await controller.getVisibleRegion();
|
final bounds = await controller.getVisibleRegion();
|
||||||
unawaited(
|
unawaited(
|
||||||
_reloadMutex.run(() async {
|
_reloadMutex.run(() async {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import 'package:immich_mobile/routing/router.dart';
|
|||||||
import 'package:immich_mobile/services/api.service.dart';
|
import 'package:immich_mobile/services/api.service.dart';
|
||||||
import 'package:immich_mobile/services/share_intent_service.dart';
|
import 'package:immich_mobile/services/share_intent_service.dart';
|
||||||
import 'package:immich_mobile/services/upload.service.dart';
|
import 'package:immich_mobile/services/upload.service.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
import 'package:path/path.dart';
|
import 'package:path/path.dart';
|
||||||
|
|
||||||
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
final shareIntentUploadProvider = StateNotifierProvider<ShareIntentUploadStateNotifier, List<ShareIntentAttachment>>(
|
||||||
@@ -25,6 +26,7 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
|||||||
final AppRouter router;
|
final AppRouter router;
|
||||||
final UploadService _uploadService;
|
final UploadService _uploadService;
|
||||||
final ShareIntentService _shareIntentService;
|
final ShareIntentService _shareIntentService;
|
||||||
|
final Logger _logger = Logger('ShareIntentUploadStateNotifier');
|
||||||
|
|
||||||
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
|
ShareIntentUploadStateNotifier(this.router, this._uploadService, this._shareIntentService) : super([]) {
|
||||||
_uploadService.taskStatusStream.listen(_updateUploadStatus);
|
_uploadService.taskStatusStream.listen(_updateUploadStatus);
|
||||||
@@ -86,6 +88,21 @@ class ShareIntentUploadStateNotifier extends StateNotifier<List<ShareIntentAttac
|
|||||||
for (final attachment in state)
|
for (final attachment in state)
|
||||||
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
|
if (attachment.id == taskId.toInt()) attachment.copyWith(status: uploadStatus) else attachment,
|
||||||
];
|
];
|
||||||
|
|
||||||
|
if (task.status == TaskStatus.failed) {
|
||||||
|
String? error;
|
||||||
|
final exception = task.exception;
|
||||||
|
if (exception != null && exception is TaskHttpException) {
|
||||||
|
final message = tryJsonDecode(exception.description)?['message'] as String?;
|
||||||
|
if (message != null) {
|
||||||
|
final responseCode = exception.httpResponseCode;
|
||||||
|
error = "${exception.exceptionType}, response code $responseCode: $message";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
error ??= task.exception?.toString();
|
||||||
|
|
||||||
|
_logger.warning("Upload failed for asset: ${task.task.filename}, error: $error");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _taskProgressCallback(TaskProgressUpdate update) {
|
void _taskProgressCallback(TaskProgressUpdate update) {
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ void main() {
|
|||||||
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
|
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('uses fetched asset orientation when dimensions are missing on Android', () async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
|
||||||
|
// Original asset has default orientation 0, but dimensions are missing
|
||||||
|
final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0);
|
||||||
|
|
||||||
|
// Fetched asset has 90° orientation and proper dimensions
|
||||||
|
final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90);
|
||||||
|
|
||||||
|
when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset);
|
||||||
|
|
||||||
|
final result = await sut.getAspectRatio(localAsset);
|
||||||
|
|
||||||
|
// Should flip dimensions since fetched asset has 90° orientation
|
||||||
|
expect(result, 1080 / 1920);
|
||||||
|
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('returns 1.0 when dimensions are still unavailable after fetching', () async {
|
test('returns 1.0 when dimensions are still unavailable after fetching', () async {
|
||||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
|
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
|
||||||
|
|
||||||
@@ -112,7 +131,9 @@ void main() {
|
|||||||
expect(result, 1.0);
|
expect(result, 1.0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('handles local asset with remoteId and uses exif from remote', () async {
|
test('handles local asset with remoteId using local orientation not remote exif', () async {
|
||||||
|
// When a LocalAsset has a remoteId (merged), we should use local orientation
|
||||||
|
// because the width/height come from the local asset (pre-corrected on iOS)
|
||||||
final localAsset = TestUtils.createLocalAsset(
|
final localAsset = TestUtils.createLocalAsset(
|
||||||
id: 'local-1',
|
id: 'local-1',
|
||||||
remoteId: 'remote-1',
|
remoteId: 'remote-1',
|
||||||
@@ -121,9 +142,24 @@ void main() {
|
|||||||
orientation: 0,
|
orientation: 0,
|
||||||
);
|
);
|
||||||
|
|
||||||
final exif = const ExifInfo(orientation: '6');
|
final result = await sut.getAspectRatio(localAsset);
|
||||||
|
|
||||||
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
|
expect(result, 1920 / 1080);
|
||||||
|
// Should not call remote exif for LocalAsset
|
||||||
|
verifyNever(() => mockRemoteAssetRepository.getExif(any()));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('handles local asset with remoteId and 90 degree rotation on Android', () async {
|
||||||
|
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||||
|
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||||
|
|
||||||
|
final localAsset = TestUtils.createLocalAsset(
|
||||||
|
id: 'local-1',
|
||||||
|
remoteId: 'remote-1',
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
orientation: 90,
|
||||||
|
);
|
||||||
|
|
||||||
final result = await sut.getAspectRatio(localAsset);
|
final result = await sut.getAspectRatio(localAsset);
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,13 @@ export class MaintenanceWebsocketRepository implements OnGatewayConnection, OnGa
|
|||||||
|
|
||||||
afterInit(websocketServer: Server) {
|
afterInit(websocketServer: Server) {
|
||||||
this.logger.log('Initialized websocket server');
|
this.logger.log('Initialized websocket server');
|
||||||
websocketServer.on('AppRestart', () => this.appRepository.exitApp());
|
|
||||||
|
websocketServer.on('AppRestart', (event: ArgsOf<'AppRestart'>, ack?: (ok: 'ok') => void) => {
|
||||||
|
this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`);
|
||||||
|
|
||||||
|
ack?.('ok');
|
||||||
|
this.appRepository.exitApp();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
|
clientBroadcast<T extends keyof ClientEventMap>(event: T, ...data: ClientEventMap[T]) {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { createAdapter } from '@socket.io/redis-adapter';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { Server as SocketIO } from 'socket.io';
|
||||||
import { ExitCode } from 'src/enum';
|
import { ExitCode } from 'src/enum';
|
||||||
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
import { AppRestartEvent } from 'src/repositories/event.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppRepository {
|
export class AppRepository {
|
||||||
@@ -17,4 +22,26 @@ export class AppRepository {
|
|||||||
setCloseFn(fn: () => Promise<void>) {
|
setCloseFn(fn: () => Promise<void>) {
|
||||||
this.closeFn = fn;
|
this.closeFn = fn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async sendOneShotAppRestart(state: AppRestartEvent): Promise<void> {
|
||||||
|
const server = new SocketIO();
|
||||||
|
const { redis } = new ConfigRepository().getEnv();
|
||||||
|
const pubClient = new Redis({ ...redis, lazyConnect: true });
|
||||||
|
const subClient = pubClient.duplicate();
|
||||||
|
|
||||||
|
await Promise.all([pubClient.connect(), subClient.connect()]);
|
||||||
|
|
||||||
|
server.adapter(createAdapter(pubClient, subClient));
|
||||||
|
|
||||||
|
// => corresponds to notification.service.ts#onAppRestart
|
||||||
|
server.emit('AppRestartV1', state, async () => {
|
||||||
|
const responses = await server.serverSideEmitWithAck('AppRestart', state);
|
||||||
|
if (responses.some((response) => response !== 'ok')) {
|
||||||
|
throw new Error("One or more node(s) returned a non-'ok' response to our restart request!");
|
||||||
|
}
|
||||||
|
|
||||||
|
pubClient.disconnect();
|
||||||
|
subClient.disconnect();
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ describe(CliService.name, () => {
|
|||||||
alreadyDisabled: true,
|
alreadyDisabled: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0);
|
||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0);
|
expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledTimes(0);
|
expect(mocks.event.emit).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
@@ -99,6 +100,7 @@ describe(CliService.name, () => {
|
|||||||
alreadyDisabled: false,
|
alreadyDisabled: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled();
|
||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
|
||||||
isMaintenanceMode: false,
|
isMaintenanceMode: false,
|
||||||
});
|
});
|
||||||
@@ -114,6 +116,7 @@ describe(CliService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalledTimes(0);
|
||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0);
|
expect(mocks.systemMetadata.set).toHaveBeenCalledTimes(0);
|
||||||
expect(mocks.event.emit).toHaveBeenCalledTimes(0);
|
expect(mocks.event.emit).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
@@ -126,6 +129,7 @@ describe(CliService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(mocks.app.sendOneShotAppRestart).toHaveBeenCalled();
|
||||||
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
|
expect(mocks.systemMetadata.set).toHaveBeenCalledWith(SystemMetadataKey.MaintenanceMode, {
|
||||||
isMaintenanceMode: true,
|
isMaintenanceMode: true,
|
||||||
secret: expect.stringMatching(/^\w{128}$/),
|
secret: expect.stringMatching(/^\w{128}$/),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
|||||||
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, sendOneShotAppRestart } from 'src/utils/maintenance';
|
import { createMaintenanceLoginUrl, generateMaintenanceSecret } from 'src/utils/maintenance';
|
||||||
import { getExternalDomain } from 'src/utils/misc';
|
import { getExternalDomain } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -55,8 +55,7 @@ export class CliService extends BaseService {
|
|||||||
|
|
||||||
const state = { isMaintenanceMode: false as const };
|
const state = { isMaintenanceMode: false as const };
|
||||||
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
|
await this.systemMetadataRepository.set(SystemMetadataKey.MaintenanceMode, state);
|
||||||
|
await this.appRepository.sendOneShotAppRestart(state);
|
||||||
sendOneShotAppRestart(state);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
alreadyDisabled: false,
|
alreadyDisabled: false,
|
||||||
@@ -89,7 +88,7 @@ export class CliService extends BaseService {
|
|||||||
secret,
|
secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
sendOneShotAppRestart({
|
await this.appRepository.sendOneShotAppRestart({
|
||||||
isMaintenanceMode: true,
|
isMaintenanceMode: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SystemMetadataKey } from 'src/enum';
|
||||||
|
import { ArgOf } from 'src/repositories/event.repository';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { MaintenanceModeState } from 'src/types';
|
import { MaintenanceModeState } from 'src/types';
|
||||||
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
import { createMaintenanceLoginUrl, generateMaintenanceSecret, signMaintenanceJwt } from 'src/utils/maintenance';
|
||||||
@@ -31,7 +32,10 @@ export class MaintenanceService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'AppRestart', server: true })
|
@OnEvent({ name: 'AppRestart', server: true })
|
||||||
onRestart(): void {
|
onRestart(event: ArgOf<'AppRestart'>, ack?: (ok: 'ok') => void): void {
|
||||||
|
this.logger.log(`Restarting due to event... ${JSON.stringify(event)}`);
|
||||||
|
|
||||||
|
ack?.('ok');
|
||||||
this.appRepository.exitApp();
|
this.appRepository.exitApp();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,55 +1,6 @@
|
|||||||
import { createAdapter } from '@socket.io/redis-adapter';
|
|
||||||
import Redis from 'ioredis';
|
|
||||||
import { SignJWT } from 'jose';
|
import { SignJWT } from 'jose';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { Server as SocketIO } from 'socket.io';
|
|
||||||
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
import { MaintenanceAuthDto } from 'src/dtos/maintenance.dto';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
|
||||||
import { AppRestartEvent } from 'src/repositories/event.repository';
|
|
||||||
|
|
||||||
export function sendOneShotAppRestart(state: AppRestartEvent): void {
|
|
||||||
const server = new SocketIO();
|
|
||||||
const { redis } = new ConfigRepository().getEnv();
|
|
||||||
const pubClient = new Redis(redis);
|
|
||||||
const subClient = pubClient.duplicate();
|
|
||||||
server.adapter(createAdapter(pubClient, subClient));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Keep trying until we manage to stop Immich
|
|
||||||
*
|
|
||||||
* Sometimes there appear to be communication
|
|
||||||
* issues between to the other servers.
|
|
||||||
*
|
|
||||||
* This issue only occurs with this method.
|
|
||||||
*/
|
|
||||||
async function tryTerminate() {
|
|
||||||
while (true) {
|
|
||||||
try {
|
|
||||||
const responses = await server.serverSideEmitWithAck('AppRestart', state);
|
|
||||||
if (responses.length > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
console.error('Encountered an error while telling Immich to stop.');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.info(
|
|
||||||
"\nIt doesn't appear that Immich stopped, trying again in a moment.\nIf Immich is already not running, you can ignore this error.",
|
|
||||||
);
|
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 1e3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// => corresponds to notification.service.ts#onAppRestart
|
|
||||||
server.emit('AppRestartV1', state, () => {
|
|
||||||
void tryTerminate().finally(() => {
|
|
||||||
pubClient.disconnect();
|
|
||||||
subClient.disconnect();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createMaintenanceLoginUrl(
|
export async function createMaintenanceLoginUrl(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte';
|
import SettingDropdown from '../components/shared-components/settings/setting-dropdown.svelte';
|
||||||
import { SlideshowLook, SlideshowNavigation, slideshowStore } from '../stores/slideshow.store';
|
import { SlideshowLook, SlideshowNavigation, SlideshowState, slideshowStore } from '../stores/slideshow.store';
|
||||||
|
|
||||||
const {
|
const {
|
||||||
slideshowDelay,
|
slideshowDelay,
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
slideshowLook,
|
slideshowLook,
|
||||||
slideshowTransition,
|
slideshowTransition,
|
||||||
slideshowAutoplay,
|
slideshowAutoplay,
|
||||||
|
slideshowState,
|
||||||
} = slideshowStore;
|
} = slideshowStore;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -69,6 +70,7 @@
|
|||||||
$slideshowLook = tempSlideshowLook;
|
$slideshowLook = tempSlideshowLook;
|
||||||
$slideshowTransition = tempSlideshowTransition;
|
$slideshowTransition = tempSlideshowTransition;
|
||||||
$slideshowAutoplay = tempSlideshowAutoplay;
|
$slideshowAutoplay = tempSlideshowAutoplay;
|
||||||
|
$slideshowState = SlideshowState.PlaySlideshow;
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user