diff --git a/lib/data/repositories/offline_repository.dart b/lib/data/repositories/offline_repository.dart index a49d71ea..1ac6081c 100644 --- a/lib/data/repositories/offline_repository.dart +++ b/lib/data/repositories/offline_repository.dart @@ -256,4 +256,24 @@ class OfflineRepository { Map rowToRawData(DownloadedItem row) { return jsonDecode(row.metadataJson) as Map; } + + Future updateItemPaths({ + required String itemId, + required String serverId, + String? localFilePath, + String? posterPath, + String? backdropPath, + String? logoPath, + String? thumbPath, + }) async { + await (_db.update(_db.downloadedItems) + ..where((t) => t.itemId.equals(itemId) & t.serverId.equals(serverId))) + .write(DownloadedItemsCompanion( + localFilePath: localFilePath != null ? Value(localFilePath) : const Value.absent(), + posterPath: posterPath != null ? Value(posterPath) : const Value.absent(), + backdropPath: backdropPath != null ? Value(backdropPath) : const Value.absent(), + logoPath: logoPath != null ? Value(logoPath) : const Value.absent(), + thumbPath: thumbPath != null ? Value(thumbPath) : const Value.absent(), + )); + } } diff --git a/lib/di/injection.dart b/lib/di/injection.dart index e0cd0798..e6a25d6d 100644 --- a/lib/di/injection.dart +++ b/lib/di/injection.dart @@ -1,5 +1,6 @@ import 'package:get_it/get_it.dart'; import 'package:jellyfin_preference/jellyfin_preference.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart' as pkg; import 'package:server_core/server_core.dart'; @@ -386,9 +387,9 @@ Future configureDependencies() async { final storagePath = StoragePathService(); getIt.registerSingleton(storagePath); getIt.registerSingleton(OfflineDatabase(openConnection())); - getIt.registerSingleton( - OfflineRepository(getIt()), - ); + final offlineRepo = OfflineRepository(getIt()); + getIt.registerSingleton(offlineRepo); + await _migrateIosPaths(offlineRepo); final connectivityService = ConnectivityService(); connectivityService.initialize(); @@ -408,3 +409,47 @@ Future configureDependencies() async { ), ); } + +String? migrateIosPath(String? storedPath, String currentDocsPath) { + if (storedPath == null) return null; + final docsIndex = storedPath.indexOf('/Documents/'); + if (docsIndex == -1) return storedPath; + final relativePath = storedPath.substring(docsIndex + '/Documents/'.length); + return '$currentDocsPath/$relativePath'; +} + +Future _migrateIosPaths(OfflineRepository repo) async { + if (!PlatformDetection.isIOS) return; + + try { + final docs = await getApplicationDocumentsDirectory(); + final currentDocsPath = docs.path; + + final items = await repo.getItems(); + for (final item in items) { + final newLocalFilePath = migrateIosPath(item.localFilePath, currentDocsPath); + final newPosterPath = migrateIosPath(item.posterPath, currentDocsPath); + final newBackdropPath = migrateIosPath(item.backdropPath, currentDocsPath); + final newLogoPath = migrateIosPath(item.logoPath, currentDocsPath); + final newThumbPath = migrateIosPath(item.thumbPath, currentDocsPath); + + if (newLocalFilePath != item.localFilePath || + newPosterPath != item.posterPath || + newBackdropPath != item.backdropPath || + newLogoPath != item.logoPath || + newThumbPath != item.thumbPath) { + await repo.updateItemPaths( + itemId: item.itemId, + serverId: item.serverId, + localFilePath: newLocalFilePath, + posterPath: newPosterPath, + backdropPath: newBackdropPath, + logoPath: newLogoPath, + thumbPath: newThumbPath, + ); + } + } + } catch (_) { + // Fail-silent to not block startup if anything goes wrong + } +} diff --git a/test/util/path_migration_test.dart b/test/util/path_migration_test.dart new file mode 100644 index 00000000..6f1aa8e6 --- /dev/null +++ b/test/util/path_migration_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:moonfin/di/injection.dart' show migrateIosPath; + +void main() { + group('iOS Path Migration', () { + test('migrateIosPath returns null if input is null', () { + expect(migrateIosPath(null, '/new/path/Documents'), isNull); + }); + + test('migrateIosPath returns original path if /Documents/ is not found', () { + const path = '/some/other/path/file.mp4'; + expect(migrateIosPath(path, '/new/path/Documents'), path); + }); + + test('migrateIosPath translates path if /Documents/ is found', () { + const oldPath = '/var/mobile/Containers/Data/Application/OLD-UUID-1234/Documents/Moonfin/Movies/video.mp4'; + const currentDocsPath = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents'; + const expected = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents/Moonfin/Movies/video.mp4'; + + expect(migrateIosPath(oldPath, currentDocsPath), expected); + }); + + test('migrateIosPath handles nested folder paths correctly', () { + const oldPath = '/var/mobile/Containers/Data/Application/OLD-UUID-1234/Documents/Moonfin/images/poster.png'; + const currentDocsPath = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents'; + const expected = '/var/mobile/Containers/Data/Application/NEW-UUID-5678/Documents/Moonfin/images/poster.png'; + + expect(migrateIosPath(oldPath, currentDocsPath), expected); + }); + }); +}