immich/mobile/ios/Runner/Platform/MediaManager.swift

174 lines
6 KiB
Swift

import Photos
class WrapperAsset: Hashable, Equatable {
var asset: Asset
init(with asset: Asset) {
self.asset = asset
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.asset.id)
}
static func == (lhs: WrapperAsset, rhs: WrapperAsset) -> Bool {
return lhs.asset.id == rhs.asset.id
}
}
class MediaManager {
private let _defaults: UserDefaults
private let _changeTokenKey = "immich:changeToken"
init(with defaults: UserDefaults = .standard) {
self._defaults = defaults
}
@available(iOS 16, *)
func _getChangeToken() -> PHPersistentChangeToken? {
guard let encodedToken = _defaults.data(forKey: _changeTokenKey) else {
print("MediaManager::_getChangeToken: Change token not available in UserDefaults")
return nil
}
do {
return try NSKeyedUnarchiver.unarchivedObject(ofClass: PHPersistentChangeToken.self, from: encodedToken)
} catch {
print("MediaManager::_getChangeToken: Cannot decode the token from UserDefaults")
return nil
}
}
@available(iOS 16, *)
func _saveChangeToken(token: PHPersistentChangeToken) -> Void {
do {
let encodedToken = try NSKeyedArchiver.archivedData(withRootObject: token, requiringSecureCoding: true)
_defaults.set(encodedToken, forKey: _changeTokenKey)
print("MediaManager::_setChangeToken: Change token saved to UserDefaults")
} catch {
print("MediaManager::_setChangeToken: Failed to persist the token to UserDefaults: \(error)")
}
}
@available(iOS 16, *)
func checkpointSync(completion: @escaping (Result<Void, any Error>) -> Void) {
_saveChangeToken(token: PHPhotoLibrary.shared().currentChangeToken)
completion(.success(()))
}
@available(iOS 16, *)
func shouldFullSync(completion: @escaping (Result<Bool, Error>) -> Void) {
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
// When we do not have access to photo library, return true to fallback to old sync
completion(.success(true))
return
}
guard let storedToken = _getChangeToken() else {
// No token exists, perform the initial full sync
print("MediaManager::shouldUseOldSync: No token found. Full sync required")
completion(.success(true))
return
}
do {
_ = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
completion(.success(false))
} catch {
// fallback to using old sync when we cannot detect changes using the available token
print("MediaManager::shouldUseOldSync: fetchPersistentChanges failed with error (\(error))")
completion(.success(true))
}
}
@available(iOS 16, *)
func getMediaChanges(completion: @escaping (Result<SyncDelta, Error>) -> Void) {
guard PHPhotoLibrary.authorizationStatus(for: .readWrite) == .authorized else {
completion(.failure(PigeonError(code: "NO_AUTH", message: "No photo library access", details: nil)))
return
}
guard let storedToken = _getChangeToken() else {
// No token exists, definitely need a full sync
print("MediaManager::getMediaChanges: No token found")
completion(.failure(PigeonError(code: "NO_TOKEN", message: "No stored change token", details: nil)))
return
}
let currentToken = PHPhotoLibrary.shared().currentChangeToken
if storedToken == currentToken {
completion(.success(SyncDelta(hasChanges: false, updates: [], deletes: [])))
return
}
do {
let result = try PHPhotoLibrary.shared().fetchPersistentChanges(since: storedToken)
let dateFormatter = ISO8601DateFormatter()
dateFormatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
var updatedArr: Set<WrapperAsset> = []
var deletedArr: Set<String> = []
for changes in result {
let details = try changes.changeDetails(for: PHObjectType.asset)
let updated = details.updatedLocalIdentifiers.union(details.insertedLocalIdentifiers)
let deleted = details.deletedLocalIdentifiers
let options = PHFetchOptions()
options.includeHiddenAssets = false
let updatedAssets = PHAsset.fetchAssets(withLocalIdentifiers: Array(updated), options: options)
updatedAssets.enumerateObjects { (asset, _, _) in
let id = asset.localIdentifier
let name = PHAssetResource.assetResources(for: asset).first?.originalFilename ?? asset.title()
let type: Int64 = Int64(asset.mediaType.rawValue)
let createdAt = asset.creationDate.map { dateFormatter.string(from: $0) }
let updatedAt = asset.modificationDate.map { dateFormatter.string(from: $0) }
let durationInSeconds: Int64 = Int64(asset.duration)
let domainAsset = WrapperAsset(with: Asset(
id: id,
name: name,
type: type,
createdAt: createdAt,
updatedAt: updatedAt,
durationInSeconds: durationInSeconds,
albumIds: self._getAlbumIds(forAsset: asset)
))
updatedArr.insert(domainAsset)
}
deletedArr.formUnion(deleted)
}
let delta = SyncDelta(hasChanges: true, updates: Array(updatedArr.map { $0.asset }), deletes: Array(deletedArr))
completion(.success(delta))
return
} catch {
print("MediaManager::getMediaChanges: Error fetching persistent changes: \(error)")
completion(.failure(PigeonError(code: "3", message: error.localizedDescription, details: nil)))
return
}
}
@available(iOS 16, *)
func _getAlbumIds(forAsset: PHAsset) -> [String] {
var albumIds: [String] = []
let albumTypes: [PHAssetCollectionType] = [.album, .smartAlbum]
albumTypes.forEach { type in
let collections = PHAssetCollection.fetchAssetCollectionsContaining(forAsset, with: type, options: nil)
collections.enumerateObjects { (album, _, _) in
albumIds.append(album.localIdentifier)
}
}
return albumIds
}
}