fix: handle corrupted download paths missing leading slash

This commit is contained in:
edde746
2026-02-27 19:05:05 +01:00
parent 5ff5441baa
commit 03d62d2e2b
4 changed files with 82 additions and 43 deletions
+31 -26
View File
@@ -1,3 +1,4 @@
import '../utils/app_logger.dart';
import '../utils/codec_utils.dart'; import '../utils/codec_utils.dart';
class PlexMediaInfo { class PlexMediaInfo {
@@ -31,32 +32,36 @@ class PlexMediaInfo {
if (streams != null) { if (streams != null) {
for (final s in streams) { for (final s in streams) {
final streamType = s['streamType'] as int?; try {
if (streamType == 2) { final streamType = s['streamType'] as int?;
audioTracks.add(PlexAudioTrack( if (streamType == 2) {
id: s['id'] as int, audioTracks.add(PlexAudioTrack(
index: s['index'] as int?, id: s['id'] as int,
codec: s['codec'] as String?, index: s['index'] as int?,
language: s['language'] as String?, codec: s['codec'] as String?,
languageCode: s['languageCode'] as String?, language: s['language'] as String?,
title: s['title'] as String?, languageCode: s['languageCode'] as String?,
displayTitle: s['displayTitle'] as String?, title: s['title'] as String?,
channels: s['channels'] as int?, displayTitle: s['displayTitle'] as String?,
selected: s['selected'] == 1 || s['selected'] == true, channels: s['channels'] as int?,
)); selected: s['selected'] == 1 || s['selected'] == true,
} else if (streamType == 3) { ));
subtitleTracks.add(PlexSubtitleTrack( } else if (streamType == 3) {
id: s['id'] as int, subtitleTracks.add(PlexSubtitleTrack(
index: s['index'] as int?, id: s['id'] as int,
codec: s['codec'] as String?, index: s['index'] as int?,
language: s['language'] as String?, codec: s['codec'] as String?,
languageCode: s['languageCode'] as String?, language: s['language'] as String?,
title: s['title'] as String?, languageCode: s['languageCode'] as String?,
displayTitle: s['displayTitle'] as String?, title: s['title'] as String?,
selected: s['selected'] == 1 || s['selected'] == true, displayTitle: s['displayTitle'] as String?,
forced: s['forced'] == 1, selected: s['selected'] == 1 || s['selected'] == true,
key: s['key'] as String?, forced: s['forced'] == 1,
)); key: s['key'] as String?,
));
}
} catch (e) {
appLogger.d('Skipping malformed stream in cached metadata', error: e);
} }
} }
} }
+2
View File
@@ -1156,6 +1156,8 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
if (metadataJson != null) { if (metadataJson != null) {
mediaInfo = PlexMediaInfo.fromMetadataJson(metadataJson); 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) { } catch (e) {
appLogger.d('Could not load cached media info for offline playback', error: e); appLogger.d('Could not load cached media info for offline playback', error: e);
+20 -6
View File
@@ -311,27 +311,41 @@ class DownloadManagerService {
// One-time migration: normalize stored file paths that may contain a // One-time migration: normalize stored file paths that may contain a
// doubled base-dir prefix from an earlier bug in the recovery callback. // 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; 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(); final allItems = await _database.select(_database.downloadedMedia).get();
var fixed = 0; var fixed = 0;
for (final item in allItems) { for (final item in allItems) {
if (item.videoFilePath != null) { if (item.videoFilePath != null) {
final normalized = await _storageService.toRelativePath(item.videoFilePath!); final vfp = item.videoFilePath!;
if (normalized != 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); await _database.updateVideoFilePath(item.globalKey, normalized);
fixed++; fixed++;
} }
} }
if (item.thumbPath != null) { if (item.thumbPath != null) {
final normalized = await _storageService.toRelativePath(item.thumbPath!); final tp = item.thumbPath!;
if (normalized != 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); await _database.updateArtworkPaths(globalKey: item.globalKey, thumbPath: normalized);
} }
} }
} }
if (fixed > 0) appLogger.i('Normalized $fixed corrupted download path(s)'); 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' // Scan drift for orphaned items stuck in 'downloading'
+29 -11
View File
@@ -5,6 +5,7 @@ import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../models/plex_metadata.dart'; import '../models/plex_metadata.dart';
import '../utils/app_logger.dart';
import '../utils/formatters.dart'; import '../utils/formatters.dart';
import 'settings_service.dart'; import 'settings_service.dart';
import 'saf_storage_service.dart'; import 'saf_storage_service.dart';
@@ -398,25 +399,42 @@ class DownloadStorageService {
} }
/// Convert a potentially absolute path (from old database entries) to absolute /// 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<String> ensureAbsolutePath(String storedPath) async { Future<String> ensureAbsolutePath(String storedPath) async {
appLogger.d('ensureAbsolutePath: input="$storedPath", isAbsolute=${path.isAbsolute(storedPath)}');
String result;
if (path.isAbsolute(storedPath)) { if (path.isAbsolute(storedPath)) {
// Already absolute - check if file exists at this path // Already absolute - check if file exists at this path
if (await File(storedPath).exists()) { 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 } else {
// Extract the relative portion (everything after 'downloads/') // 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/'); final downloadsIndex = storedPath.indexOf('downloads/');
if (downloadsIndex != -1) { if (downloadsIndex > 0) {
final relativePart = storedPath.substring(downloadsIndex); result = await toAbsolutePath(storedPath.substring(downloadsIndex));
return await toAbsolutePath(relativePart); } 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 /// Calculate total storage used by downloads