mirror of
https://github.com/immich-app/immich.git
synced 2026-06-24 07:28:30 -07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d954591ca8 |
@@ -446,7 +446,6 @@ class SyncStreamService {
|
||||
|
||||
await _syncStreamRepository.updateAssetsV1([asset], debugLabel: 'websocket-edit');
|
||||
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||
await _refreshAssetOcrAndFaces(asset.id);
|
||||
|
||||
_logger.info(
|
||||
'Successfully processed AssetEditReadyV1 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||
@@ -485,7 +484,6 @@ class SyncStreamService {
|
||||
|
||||
await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
|
||||
await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
|
||||
await _refreshAssetOcrAndFaces(asset.id);
|
||||
|
||||
_logger.info(
|
||||
'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
|
||||
@@ -495,22 +493,6 @@ class SyncStreamService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refreshAssetOcrAndFaces(String assetId) async {
|
||||
try {
|
||||
final ocr = await _api.assetsApi.getAssetOcr(assetId);
|
||||
await _syncStreamRepository.replaceAssetOcr(assetId, ocr ?? const []);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error refreshing OCR for asset $assetId", error, stackTrace);
|
||||
}
|
||||
|
||||
try {
|
||||
final faces = await _api.facesApi.getFaces(assetId);
|
||||
await _syncStreamRepository.replaceAssetFaces(assetId, faces ?? const []);
|
||||
} catch (error, stackTrace) {
|
||||
_logger.severe("Error refreshing faces for asset $assetId", error, stackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleRemoteDeleted(Iterable<String> remoteIds) async {
|
||||
if (remoteIds.isEmpty) {
|
||||
return Future.value();
|
||||
|
||||
@@ -896,71 +896,6 @@ class SyncStreamRepository extends DriftDatabaseRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces all OCR rows for [assetId] with [data] (e.g. after an asset edit re-runs OCR).
|
||||
Future<void> replaceAssetOcr(String assetId, Iterable<AssetOcrResponseDto> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
batch.deleteWhere(_db.assetOcrEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
for (final ocr in data) {
|
||||
batch.insert(
|
||||
_db.assetOcrEntity,
|
||||
AssetOcrEntityCompanion(
|
||||
id: Value(ocr.id),
|
||||
assetId: Value(ocr.assetId),
|
||||
recognizedText: Value(ocr.text),
|
||||
x1: Value(ocr.x1),
|
||||
y1: Value(ocr.y1),
|
||||
x2: Value(ocr.x2),
|
||||
y2: Value(ocr.y2),
|
||||
x3: Value(ocr.x3),
|
||||
y3: Value(ocr.y3),
|
||||
x4: Value(ocr.x4),
|
||||
y4: Value(ocr.y4),
|
||||
boxScore: Value(ocr.boxScore),
|
||||
textScore: Value(ocr.textScore),
|
||||
isVisible: const Value(true),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: replaceAssetOcr', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> replaceAssetFaces(String assetId, Iterable<AssetFaceResponseDto> data) async {
|
||||
try {
|
||||
await _db.batch((batch) {
|
||||
batch.deleteWhere(_db.assetFaceEntity, (row) => row.assetId.equals(assetId));
|
||||
|
||||
for (final face in data) {
|
||||
batch.insert(
|
||||
_db.assetFaceEntity,
|
||||
AssetFaceEntityCompanion(
|
||||
id: Value(face.id),
|
||||
assetId: Value(assetId),
|
||||
personId: Value(face.person?.id),
|
||||
imageWidth: Value(face.imageWidth),
|
||||
imageHeight: Value(face.imageHeight),
|
||||
boundingBoxX1: Value(face.boundingBoxX1),
|
||||
boundingBoxY1: Value(face.boundingBoxY1),
|
||||
boundingBoxX2: Value(face.boundingBoxX2),
|
||||
boundingBoxY2: Value(face.boundingBoxY2),
|
||||
sourceType: Value(face.sourceType.orElse(null)?.value ?? SourceType.machineLearning.value),
|
||||
isVisible: const Value(true),
|
||||
deletedAt: const Value(null),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (error, stack) {
|
||||
_logger.severe('Error: replaceAssetFaces', error, stack);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pruneAssets() async {
|
||||
try {
|
||||
await _db.transaction(() async {
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:immich_mobile/infrastructure/repositories/network.repository.dar
|
||||
import 'package:immich_mobile/models/server_info/server_version.model.dart';
|
||||
import 'package:immich_mobile/providers/auth.provider.dart';
|
||||
import 'package:immich_mobile/providers/background_sync.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/ocr.provider.dart';
|
||||
import 'package:immich_mobile/providers/infrastructure/settings.provider.dart';
|
||||
import 'package:immich_mobile/providers/server_info.provider.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
@@ -182,34 +181,11 @@ class WebsocketNotifier extends StateNotifier<WebsocketState> {
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReadyV1(dynamic data) {
|
||||
final assetId = _assetIdFromEditReady(data);
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data).whenComplete(() => _onAssetEditApplied(assetId)),
|
||||
);
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV1(data));
|
||||
}
|
||||
|
||||
void _handleSyncAssetEditReadyV2(dynamic data) {
|
||||
final assetId = _assetIdFromEditReady(data);
|
||||
unawaited(
|
||||
_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data).whenComplete(() => _onAssetEditApplied(assetId)),
|
||||
);
|
||||
}
|
||||
|
||||
String? _assetIdFromEditReady(dynamic data) {
|
||||
if (data is Map && data['asset'] is Map) {
|
||||
final id = (data['asset'] as Map)['id'];
|
||||
return id is String ? id : null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The edit handler refreshes OCR/faces in the drift DB from a background isolate,
|
||||
/// so the main-isolate UI providers must be invalidated here to re-read the new data.
|
||||
void _onAssetEditApplied(String? assetId) {
|
||||
if (assetId == null) {
|
||||
return;
|
||||
}
|
||||
_ref.invalidate(ocrAssetProvider(assetId));
|
||||
unawaited(_ref.read(backgroundSyncProvider).syncWebsocketEditV2(data));
|
||||
}
|
||||
|
||||
void _processBatchedAssetUploadReadyV1() {
|
||||
|
||||
@@ -36,7 +36,6 @@ class ApiService {
|
||||
late MemoriesApi memoriesApi;
|
||||
late SessionsApi sessionsApi;
|
||||
late TagsApi tagsApi;
|
||||
late FacesApi facesApi;
|
||||
|
||||
ApiService() {
|
||||
// The below line ensures that the api clients are initialized when the service is instantiated
|
||||
@@ -78,7 +77,6 @@ class ApiService {
|
||||
memoriesApi = MemoriesApi(_apiClient);
|
||||
sessionsApi = SessionsApi(_apiClient);
|
||||
tagsApi = TagsApi(_apiClient);
|
||||
facesApi = FacesApi(_apiClient);
|
||||
}
|
||||
|
||||
Future<String> resolveAndSetEndpoint(String serverUrl) async {
|
||||
|
||||
@@ -203,6 +203,76 @@
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetExifFilter",
|
||||
"title": "Filter by EXIF metadata",
|
||||
"description": "Filter assets by their EXIF properties",
|
||||
"types": ["AssetV1"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"property": {
|
||||
"title": "Property",
|
||||
"description": "EXIF property to match",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"make",
|
||||
"model",
|
||||
"exifImageWidth",
|
||||
"exifImageHeight",
|
||||
"fileSizeInByte",
|
||||
"orientation",
|
||||
"lensModel",
|
||||
"fNumber",
|
||||
"focalLength",
|
||||
"iso",
|
||||
"description",
|
||||
"fps",
|
||||
"exposureTime",
|
||||
"livePhotoCID",
|
||||
"timeZone",
|
||||
"projectionType",
|
||||
"profileDescription",
|
||||
"colorspace",
|
||||
"bitsPerSample",
|
||||
"rating"
|
||||
],
|
||||
"uiHint": {
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
"pattern": {
|
||||
"type": "string",
|
||||
"title": "Pattern",
|
||||
"description": "Text or regex pattern to match against property value",
|
||||
"uiHint": {
|
||||
"order": 2
|
||||
}
|
||||
},
|
||||
"matchType": {
|
||||
"type": "string",
|
||||
"title": "Match type",
|
||||
"enum": ["contains", "startsWith", "exact", "regex"],
|
||||
"default": "contains",
|
||||
"description": "Type of pattern matching to perform",
|
||||
"uiHint": {
|
||||
"order": 3
|
||||
}
|
||||
},
|
||||
"caseSensitive": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"title": "Case sensitive",
|
||||
"description": "Whether matching should be case-sensitive",
|
||||
"uiHint": {
|
||||
"order": 4
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["property", "pattern"]
|
||||
},
|
||||
"uiHints": ["Filter"]
|
||||
},
|
||||
{
|
||||
"name": "assetArchive",
|
||||
"title": "Archive asset",
|
||||
|
||||
Vendored
+1
@@ -15,6 +15,7 @@ declare module 'main' {
|
||||
export function assetMissingTimeZoneFilter(): I32;
|
||||
export function assetLocationFilter(): I32;
|
||||
export function assetTypeFilter(): I32;
|
||||
export function assetExifFilter(): I32;
|
||||
|
||||
// updates
|
||||
export function assetFavorite(): I32;
|
||||
|
||||
@@ -1,45 +1,46 @@
|
||||
import { wrapper } from '@immich/plugin-sdk';
|
||||
import { AssetTypeEnum, AssetVisibility, WorkflowType } from '@immich/sdk';
|
||||
|
||||
type AssetFileFilterConfig = {
|
||||
type MatchValueConfig = {
|
||||
pattern: string;
|
||||
matchType?: 'contains' | 'exact' | 'regex' | 'startsWith';
|
||||
caseSensitive?: boolean;
|
||||
};
|
||||
export const assetFileFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetFileFilterConfig>(({ data, config }) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
|
||||
const { asset } = data;
|
||||
const matchValueResult = (value: string, config: MatchValueConfig) => {
|
||||
const { pattern, matchType = 'contains', caseSensitive = false } = config;
|
||||
const searchName = caseSensitive ? value : value.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
const fileName = asset.originalFileName || '';
|
||||
const searchName = caseSensitive ? fileName : fileName.toLowerCase();
|
||||
const searchPattern = caseSensitive ? pattern : pattern.toLowerCase();
|
||||
|
||||
switch (matchType) {
|
||||
case 'contains': {
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'exact': {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
case 'startsWith': {
|
||||
return { workflow: { continue: searchName.startsWith(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'regex': {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(fileName) } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
switch (matchType) {
|
||||
case 'contains': {
|
||||
return { workflow: { continue: searchName.includes(searchPattern) } };
|
||||
}
|
||||
});
|
||||
|
||||
case 'exact': {
|
||||
return { workflow: { continue: searchName === searchPattern } };
|
||||
}
|
||||
|
||||
case 'startsWith': {
|
||||
return { workflow: { continue: searchName.startsWith(searchPattern) } };
|
||||
}
|
||||
|
||||
case 'regex': {
|
||||
const flags = caseSensitive ? '' : 'i';
|
||||
const regex = new RegExp(searchPattern, flags);
|
||||
return { workflow: { continue: regex.test(value) } };
|
||||
}
|
||||
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const assetFileFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, MatchValueConfig>(({ data, config }) =>
|
||||
matchValueResult(data.asset.originalFileName || '', config),
|
||||
);
|
||||
};
|
||||
|
||||
export const assetMissingTimeZoneFilter = () => {
|
||||
@@ -95,6 +96,40 @@ export const assetLocationFilter = () => {
|
||||
});
|
||||
};
|
||||
|
||||
type AssetExifFilterConfig = MatchValueConfig & {
|
||||
property:
|
||||
| 'make'
|
||||
| 'model'
|
||||
| 'exifImageWidth'
|
||||
| 'exifImageHeight'
|
||||
| 'fileSizeInByte'
|
||||
| 'orientation'
|
||||
| 'lensModel'
|
||||
| 'fNumber'
|
||||
| 'focalLength'
|
||||
| 'iso'
|
||||
| 'description'
|
||||
| 'fps'
|
||||
| 'exposureTime'
|
||||
| 'livePhotoCID'
|
||||
| 'timeZone'
|
||||
| 'projectionType'
|
||||
| 'profileDescription'
|
||||
| 'colorspace'
|
||||
| 'bitsPerSample'
|
||||
| 'rating';
|
||||
};
|
||||
|
||||
export const assetExifFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, AssetExifFilterConfig>(({ config, data }) => {
|
||||
if (!data.asset.exifInfo) {
|
||||
return { workflow: { continue: false } };
|
||||
}
|
||||
|
||||
return matchValueResult(String(data.asset.exifInfo[config.property] || ''), config);
|
||||
});
|
||||
};
|
||||
|
||||
export const assetTypeFilter = () => {
|
||||
return wrapper<WorkflowType.AssetV1, { allowedTypes: AssetTypeEnum[] }>(({ config, data }) => {
|
||||
return { workflow: { continue: config.allowedTypes.includes(data.asset.type) } };
|
||||
|
||||
Reference in New Issue
Block a user