1
0
mirror of https://github.com/immich-app/immich.git synced 2025-08-08 23:07:06 +02:00

refactor(mobile): more repositories (#12879)

* ExifInfoRepository
 * ActivityApiRepository
 * initial AssetApiRepository
This commit is contained in:
Fynn Petersen-Frey
2024-09-24 08:24:48 +02:00
committed by GitHub
parent 56f680ce04
commit e0fa3cdbc7
20 changed files with 392 additions and 193 deletions

View File

@ -1,41 +1,31 @@
import 'package:immich_mobile/constants/errors.dart';
import 'package:immich_mobile/interfaces/activity_api.interface.dart';
import 'package:immich_mobile/mixins/error_logger.mixin.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
class ActivityService with ErrorLoggerMixin {
final ApiService _apiService;
final IActivityApiRepository _activityApiRepository;
@override
final Logger logger = Logger("ActivityService");
ActivityService(this._apiService);
ActivityService(this._activityApiRepository);
Future<List<Activity>> getAllActivities(
String albumId, {
String? assetId,
}) async {
return logError(
() async {
final list = await _apiService.activitiesApi
.getActivities(albumId, assetId: assetId);
return list != null ? list.map(Activity.fromDto).toList() : [];
},
() => _activityApiRepository.getAll(albumId, assetId: assetId),
defaultValue: [],
errorMessage: "Failed to get all activities for album $albumId",
);
}
Future<int> getStatistics(String albumId, {String? assetId}) async {
Future<ActivityStats> getStatistics(String albumId, {String? assetId}) async {
return logError(
() async {
final dto = await _apiService.activitiesApi
.getActivityStatistics(albumId, assetId: assetId);
return dto?.comments ?? 0;
},
defaultValue: 0,
() => _activityApiRepository.getStats(albumId, assetId: assetId),
defaultValue: const ActivityStats(comments: 0),
errorMessage: "Failed to statistics for album $albumId",
);
}
@ -43,7 +33,7 @@ class ActivityService with ErrorLoggerMixin {
Future<bool> removeActivity(String id) async {
return logError(
() async {
await _apiService.activitiesApi.deleteActivity(id);
await _activityApiRepository.delete(id);
return true;
},
defaultValue: false,
@ -58,22 +48,12 @@ class ActivityService with ErrorLoggerMixin {
String? comment,
}) async {
return guardError(
() async {
final dto = await _apiService.activitiesApi.createActivity(
ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment
? ReactionType.comment
: ReactionType.like,
assetId: assetId,
comment: comment,
),
);
if (dto != null) {
return Activity.fromDto(dto);
}
throw NoResponseDtoError();
},
() => _activityApiRepository.create(
albumId,
type,
assetId: assetId,
comment: comment,
),
errorMessage: "Failed to create $type for album $albumId",
);
}

View File

@ -9,9 +9,13 @@ import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
import 'package:immich_mobile/interfaces/asset_api.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset_api.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
@ -24,6 +28,8 @@ import 'package:openapi/api.dart';
final assetServiceProvider = Provider(
(ref) => AssetService(
ref.watch(assetApiRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
ref.watch(apiServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(userServiceProvider),
@ -34,6 +40,8 @@ final assetServiceProvider = Provider(
);
class AssetService {
final IAssetApiRepository _assetApiRepository;
final IExifInfoRepository _exifInfoRepository;
final ApiService _apiService;
final SyncService _syncService;
final UserService _userService;
@ -43,6 +51,8 @@ class AssetService {
final Isar _db;
AssetService(
this._assetApiRepository,
this._exifInfoRepository,
this._apiService,
this._syncService,
this._userService,
@ -342,4 +352,46 @@ class AssetService {
log.severe("Error while syncing uploaded asset to albums", error, stack);
}
}
Future<void> setDescription(
Asset asset,
String newDescription,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
final result = await _assetApiRepository.update(
remoteAssetId,
description: newDescription,
);
final description = result.exifInfo?.description;
if (description != null) {
var exifInfo = await _exifInfoRepository.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
await _exifInfoRepository.update(exifInfo);
}
}
}
Future<String> getDescription(Asset asset) async {
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {
return "";
}
final exifInfo = await _exifInfoRepository.get(localExifId);
return exifInfo?.description ?? "";
}
}

View File

@ -1,66 +0,0 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart';
class AssetDescriptionService {
AssetDescriptionService(this._db, this._api);
final Isar _db;
final ApiService _api;
Future<void> setDescription(
Asset asset,
String newDescription,
) async {
final remoteAssetId = asset.remoteId;
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (remoteAssetId == null || localExifId == null) {
return;
}
final result = await _api.assetsApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: newDescription),
);
final description = result?.exifInfo?.description;
if (description != null) {
var exifInfo = await _db.exifInfos.get(localExifId);
if (exifInfo != null) {
exifInfo.description = description;
await _db.writeTxn(
() => _db.exifInfos.put(exifInfo),
);
}
}
}
String getAssetDescription(Asset asset) {
final localExifId = asset.exifInfo?.id;
// Guard [remoteAssetId] and [localExifId] null
if (localExifId == null) {
return "";
}
final exifInfo = _db.exifInfos.getSync(localExifId);
return exifInfo?.description ?? "";
}
}
final assetDescriptionServiceProvider = Provider(
(ref) => AssetDescriptionService(
ref.watch(dbProvider),
ref.watch(apiServiceProvider),
),
);

View File

@ -8,41 +8,46 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/interfaces/asset.interface.dart';
import 'package:immich_mobile/interfaces/exif_info.interface.dart';
import 'package:immich_mobile/interfaces/file_media.interface.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/repositories/asset.repository.dart';
import 'package:immich_mobile/repositories/exif_info.repository.dart';
import 'package:immich_mobile/repositories/file_media.repository.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:isar/isar.dart';
/// Finds duplicates originating from missing EXIF information
class BackupVerificationService {
final Isar _db;
final IFileMediaRepository _fileMediaRepository;
final IAssetRepository _assetRepository;
final IExifInfoRepository _exifInfoRepository;
BackupVerificationService(this._db, this._fileMediaRepository);
BackupVerificationService(
this._fileMediaRepository,
this._assetRepository,
this._exifInfoRepository,
);
/// Returns at most [limit] assets that were backed up without exif
Future<List<Asset>> findWronglyBackedUpAssets({int limit = 100}) async {
final owner = Store.get(StoreKey.currentUser).isarId;
final List<Asset> onlyLocal = await _db.assets
.where()
.remoteIdIsNull()
.filter()
.ownerIdEqualTo(owner)
.localIdIsNotNull()
.findAll();
final List<Asset> remoteMatches = await _getMatches(
_db.assets.where().localIdIsNull().filter().remoteIdIsNotNull(),
owner,
onlyLocal,
limit,
final List<Asset> onlyLocal = await _assetRepository.getAll(
ownerId: owner,
remote: false,
limit: limit,
);
final List<Asset> localMatches = await _getMatches(
_db.assets.where().remoteIdIsNull().filter().localIdIsNotNull(),
owner,
remoteMatches,
limit,
final List<Asset> remoteMatches = await _assetRepository.getMatches(
assets: onlyLocal,
ownerId: owner,
remote: true,
limit: limit,
);
final List<Asset> localMatches = await _assetRepository.getMatches(
assets: remoteMatches,
ownerId: owner,
remote: false,
limit: limit,
);
final List<Asset> deleteCandidates = [], originals = [];
@ -52,7 +57,7 @@ class BackupVerificationService {
localMatches,
compare: (a, b) => a.fileName.compareTo(b.fileName),
both: (a, b) async {
a.exifInfo = await _db.exifInfos.get(a.id);
a.exifInfo = await _exifInfoRepository.get(a.id);
deleteCandidates.add(a);
originals.add(b);
return false;
@ -192,35 +197,6 @@ class BackupVerificationService {
return bytes.buffer.asUint64List(start);
}
static Future<List<Asset>> _getMatches(
QueryBuilder<Asset, Asset, QAfterFilterCondition> query,
int ownerId,
List<Asset> assets,
int limit,
) =>
query
.ownerIdEqualTo(ownerId)
.anyOf(
assets,
(q, Asset a) => q
.fileNameEqualTo(a.fileName)
.and()
.durationInSecondsEqualTo(a.durationInSeconds)
.and()
.fileCreatedAtBetween(
a.fileCreatedAt.subtract(const Duration(hours: 12)),
a.fileCreatedAt.add(const Duration(hours: 12)),
)
.and()
.not()
.checksumEqualTo(a.checksum),
)
.sortByFileName()
.thenByFileCreatedAt()
.thenByFileModifiedAt()
.limit(limit)
.findAll();
static bool _sameExceptTimeZone(DateTime a, DateTime b) {
final ms = a.isAfter(b)
? a.millisecondsSinceEpoch - b.millisecondsSinceEpoch
@ -233,7 +209,8 @@ class BackupVerificationService {
final backupVerificationServiceProvider = Provider(
(ref) => BackupVerificationService(
ref.watch(dbProvider),
ref.watch(fileMediaRepositoryProvider),
ref.watch(assetRepositoryProvider),
ref.watch(exifInfoRepositoryProvider),
),
);