fix: handle corrupted download paths missing leading slash
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user