From 03d62d2e2be46be6fa5bebcffdcc80f47e7d2cfe Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Fri, 27 Feb 2026 19:05:05 +0100 Subject: [PATCH] fix: handle corrupted download paths missing leading slash --- lib/models/plex_media_info.dart | 57 ++++++++++++---------- lib/screens/video_player_screen.dart | 2 + lib/services/download_manager_service.dart | 26 +++++++--- lib/services/download_storage_service.dart | 40 ++++++++++----- 4 files changed, 82 insertions(+), 43 deletions(-) diff --git a/lib/models/plex_media_info.dart b/lib/models/plex_media_info.dart index ab2bea74..1c92a219 100644 --- a/lib/models/plex_media_info.dart +++ b/lib/models/plex_media_info.dart @@ -1,3 +1,4 @@ +import '../utils/app_logger.dart'; import '../utils/codec_utils.dart'; class PlexMediaInfo { @@ -31,32 +32,36 @@ class PlexMediaInfo { if (streams != null) { for (final s in streams) { - final streamType = s['streamType'] as int?; - if (streamType == 2) { - audioTracks.add(PlexAudioTrack( - id: s['id'] as int, - index: s['index'] as int?, - codec: s['codec'] as String?, - language: s['language'] as String?, - languageCode: s['languageCode'] as String?, - title: s['title'] as String?, - displayTitle: s['displayTitle'] as String?, - channels: s['channels'] as int?, - selected: s['selected'] == 1 || s['selected'] == true, - )); - } else if (streamType == 3) { - subtitleTracks.add(PlexSubtitleTrack( - id: s['id'] as int, - index: s['index'] as int?, - codec: s['codec'] as String?, - language: s['language'] as String?, - languageCode: s['languageCode'] as String?, - title: s['title'] as String?, - displayTitle: s['displayTitle'] as String?, - selected: s['selected'] == 1 || s['selected'] == true, - forced: s['forced'] == 1, - key: s['key'] as String?, - )); + try { + final streamType = s['streamType'] as int?; + if (streamType == 2) { + audioTracks.add(PlexAudioTrack( + id: s['id'] as int, + index: s['index'] as int?, + codec: s['codec'] as String?, + language: s['language'] as String?, + languageCode: s['languageCode'] as String?, + title: s['title'] as String?, + displayTitle: s['displayTitle'] as String?, + channels: s['channels'] as int?, + selected: s['selected'] == 1 || s['selected'] == true, + )); + } else if (streamType == 3) { + subtitleTracks.add(PlexSubtitleTrack( + id: s['id'] as int, + index: s['index'] as int?, + codec: s['codec'] as String?, + language: s['language'] as String?, + languageCode: s['languageCode'] as String?, + title: s['title'] as String?, + displayTitle: s['displayTitle'] as String?, + selected: s['selected'] == 1 || s['selected'] == true, + forced: s['forced'] == 1, + key: s['key'] as String?, + )); + } + } catch (e) { + appLogger.d('Skipping malformed stream in cached metadata', error: e); } } } diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index db002de2..ed0ea2d5 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -1156,6 +1156,8 @@ class VideoPlayerScreenState extends State with WidgetsBindin if (metadataJson != null) { mediaInfo = PlexMediaInfo.fromMetadataJson(metadataJson); } + appLogger.d('Offline media info: cached=${cached != null}, hasMedia=${metadataJson?['Media'] != null}, ' + 'audioTracks=${mediaInfo?.audioTracks.length ?? 0}, subtitleTracks=${mediaInfo?.subtitleTracks.length ?? 0}'); } } catch (e) { appLogger.d('Could not load cached media info for offline playback', error: e); diff --git a/lib/services/download_manager_service.dart b/lib/services/download_manager_service.dart index 4e6ac70e..b73b97b7 100644 --- a/lib/services/download_manager_service.dart +++ b/lib/services/download_manager_service.dart @@ -311,27 +311,41 @@ class DownloadManagerService { // One-time migration: normalize stored file paths that may contain a // doubled base-dir prefix from an earlier bug in the recovery callback. + // Re-run on v2 to also fix paths without a leading / that the v1 migration missed. final prefs = (await SettingsService.getInstance()).prefs; - if (!(prefs.getBool('download_paths_normalized') ?? false)) { + if ((prefs.getInt('download_paths_normalized_version') ?? 0) < 2) { final allItems = await _database.select(_database.downloadedMedia).get(); var fixed = 0; for (final item in allItems) { if (item.videoFilePath != null) { - final normalized = await _storageService.toRelativePath(item.videoFilePath!); - if (normalized != item.videoFilePath) { + final vfp = item.videoFilePath!; + var normalized = await _storageService.toRelativePath(vfp); + // If toRelativePath didn't help, try extracting from downloads/ onward + // for paths that lack a leading / but contain nested base-dir fragments + if (normalized == vfp) { + final idx = vfp.indexOf('downloads/'); + if (idx > 0) normalized = vfp.substring(idx); + } + appLogger.d('Path migration: videoFilePath="$vfp", normalized="$normalized"'); + if (normalized != vfp) { await _database.updateVideoFilePath(item.globalKey, normalized); fixed++; } } if (item.thumbPath != null) { - final normalized = await _storageService.toRelativePath(item.thumbPath!); - if (normalized != item.thumbPath) { + final tp = item.thumbPath!; + var normalized = await _storageService.toRelativePath(tp); + if (normalized == tp) { + final idx = tp.indexOf('downloads/'); + if (idx > 0) normalized = tp.substring(idx); + } + if (normalized != tp) { await _database.updateArtworkPaths(globalKey: item.globalKey, thumbPath: normalized); } } } if (fixed > 0) appLogger.i('Normalized $fixed corrupted download path(s)'); - await prefs.setBool('download_paths_normalized', true); + await prefs.setInt('download_paths_normalized_version', 2); } // Scan drift for orphaned items stuck in 'downloading' diff --git a/lib/services/download_storage_service.dart b/lib/services/download_storage_service.dart index 7bf3573c..1841a028 100644 --- a/lib/services/download_storage_service.dart +++ b/lib/services/download_storage_service.dart @@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import '../models/plex_metadata.dart'; +import '../utils/app_logger.dart'; import '../utils/formatters.dart'; import 'settings_service.dart'; import 'saf_storage_service.dart'; @@ -398,25 +399,42 @@ class DownloadStorageService { } /// Convert a potentially absolute path (from old database entries) to absolute - /// This handles both old absolute paths and new relative paths + /// This handles both old absolute paths and new relative paths, including + /// corrupted paths that contain nested base-dir fragments without a leading slash + /// (e.g. "data/user/0/.../app_flutter/downloads/..."). Future ensureAbsolutePath(String storedPath) async { + appLogger.d('ensureAbsolutePath: input="$storedPath", isAbsolute=${path.isAbsolute(storedPath)}'); + + String result; if (path.isAbsolute(storedPath)) { // Already absolute - check if file exists at this path if (await File(storedPath).exists()) { - return storedPath; + result = storedPath; + } else { + // File doesn't exist at absolute path - try to reconstruct + // Extract the relative portion (everything after 'downloads/') + final downloadsIndex = storedPath.indexOf('downloads/'); + if (downloadsIndex != -1) { + final relativePart = storedPath.substring(downloadsIndex); + result = await toAbsolutePath(relativePart); + } else { + // Can't reconstruct, return original + result = storedPath; + } } - // File doesn't exist at absolute path - try to reconstruct - // Extract the relative portion (everything after 'downloads/') + } else { + // Relative path — if it contains a nested base-dir fragment + // (e.g. "data/.../app_flutter/downloads/..."), extract from downloads/ onward final downloadsIndex = storedPath.indexOf('downloads/'); - if (downloadsIndex != -1) { - final relativePart = storedPath.substring(downloadsIndex); - return await toAbsolutePath(relativePart); + if (downloadsIndex > 0) { + result = await toAbsolutePath(storedPath.substring(downloadsIndex)); + } else { + result = await toAbsolutePath(storedPath); } - // Can't reconstruct, return original - return storedPath; } - // Relative path - convert to absolute - return await toAbsolutePath(storedPath); + + appLogger.d('ensureAbsolutePath: resolved="$result"'); + return result; } /// Calculate total storage used by downloads