From 0db6eb6f2e443a01bd198b09df870a96fc5736e8 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 12 Jul 2026 00:49:24 +0200 Subject: [PATCH] fix(downloads): normalize Windows sidecar paths --- .../playback_initialization_service.dart | 2 +- .../download_storage_service_test.dart | 62 ++++++++++--------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lib/services/playback_initialization_service.dart b/lib/services/playback_initialization_service.dart index cf007b14..b5b64df4 100644 --- a/lib/services/playback_initialization_service.dart +++ b/lib/services/playback_initialization_service.dart @@ -303,7 +303,7 @@ class PlaybackInitializationService { subtitles.add( SubtitleTrack.uri( - 'file://${entity.path}', + Uri.file(entity.path).toString(), title: cachedTrack?.displayTitle ?? cachedTrack?.language ?? 'Subtitle $fileName', language: cachedTrack?.languageCode, codec: cachedTrack?.codec, diff --git a/test/services/download_storage_service_test.dart b/test/services/download_storage_service_test.dart index f6f317ed..312572cf 100644 --- a/test/services/download_storage_service_test.dart +++ b/test/services/download_storage_service_test.dart @@ -148,37 +148,41 @@ void main() { expect(display, customDir.path); }); - test('falls back to default when custom path is non-writable', () async { - final settings = await SettingsService.getInstance(); + test( + 'falls back to default when custom path is non-writable', + () async { + final settings = await SettingsService.getInstance(); - // Point the custom path to a path inside a read-only parent. - final readOnlyParent = Directory(p.join(tmpRoot.path, 'readonly'))..createSync(recursive: true); - try { - // Make parent unwritable so writing inside fails. Skip if the OS - // ignores the chmod (e.g. when running as root). - await Process.run('chmod', ['000', readOnlyParent.path]); - final blocked = p.join(readOnlyParent.path, 'forbidden'); - await settings.write(SettingsService.customDownloadPathType, 'file'); - await settings.write(SettingsService.customDownloadPath, blocked); + // Point the custom path to a path inside a read-only parent. + final readOnlyParent = Directory(p.join(tmpRoot.path, 'readonly'))..createSync(recursive: true); + try { + // Make parent unwritable so writing inside fails. Skip if the OS + // ignores the chmod (e.g. when running as root). + await Process.run('chmod', ['000', readOnlyParent.path]); + final blocked = p.join(readOnlyParent.path, 'forbidden'); + await settings.write(SettingsService.customDownloadPathType, 'file'); + await settings.write(SettingsService.customDownloadPath, blocked); - final dss = DownloadStorageService.instance; - await dss.initialize(settings); + final dss = DownloadStorageService.instance; + await dss.initialize(settings); - final dir = await dss.getDownloadsDirectory(); - // Either the chmod worked → we fall back to default, - // or it didn't → we used the custom path. Both are valid; the - // important contract is that the call doesn't throw. - expect(dir.existsSync(), isTrue); - if (dir.path == blocked) { - // chmod was a no-op (root or a filesystem that ignores it). Skip the - // strict assertion — the fallback branch only runs when writes fail. - return; + final dir = await dss.getDownloadsDirectory(); + // Either the chmod worked → we fall back to default, + // or it didn't → we used the custom path. Both are valid; the + // important contract is that the call doesn't throw. + expect(dir.existsSync(), isTrue); + if (dir.path == blocked) { + // chmod was a no-op (root or a filesystem that ignores it). Skip the + // strict assertion — the fallback branch only runs when writes fail. + return; + } + expect(dir.path, p.join(p.join(tmpRoot.path, 'support'), 'downloads')); + } finally { + await Process.run('chmod', ['755', readOnlyParent.path]); } - expect(dir.path, p.join(p.join(tmpRoot.path, 'support'), 'downloads')); - } finally { - await Process.run('chmod', ['755', readOnlyParent.path]); - } - }); + }, + skip: Platform.isWindows ? 'Windows does not provide chmod permission semantics' : false, + ); test('refreshCustomPath picks up settings changes', () async { final settings = await SettingsService.getInstance(); @@ -401,7 +405,7 @@ void main() { // returns the toAbsolutePath() candidate (joined under the base dir). const stored = 'downloads/missing/never.mkv'; final resolved = await dss.ensureAbsolutePath(stored); - final expected = p.join(tmpRoot.path, 'support', stored); + final expected = p.normalize(p.join(tmpRoot.path, 'support', stored)); expect(resolved, expected); }); @@ -417,7 +421,7 @@ void main() { final resolved = await dss.ensureAbsolutePath(stored); // Falls back to the original absolute path (it doesn't exist on disk, // and no other candidate could be derived from it). - expect(resolved, stored); + expect(resolved, p.normalize(stored)); }); });