diff --git a/CHANGELOG.md b/CHANGELOG.md index d9aee728f..967687b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Fix bug scheduling mid-roll filler with content that contains one chapter - No mid-roll filler will be inserted for content with zero or one chapters - Fix thread sync bug with `HLS Segmenter` (and `MPEG-TS`) streaming modes +- Fix path replacement bug when media server path is left blank ### Added - Add automated error reporting via Bugsnag diff --git a/ErsatzTV.Core.Tests/Emby/EmbyPathReplacementServiceTests.cs b/ErsatzTV.Core.Tests/Emby/EmbyPathReplacementServiceTests.cs new file mode 100644 index 000000000..3b33a1695 --- /dev/null +++ b/ErsatzTV.Core.Tests/Emby/EmbyPathReplacementServiceTests.cs @@ -0,0 +1,271 @@ +using System.Runtime.InteropServices; +using ErsatzTV.Core.Domain; +using ErsatzTV.Core.Interfaces.Repositories; +using ErsatzTV.Core.Interfaces.Runtime; +using ErsatzTV.Core.Emby; +using FluentAssertions; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; + +namespace ErsatzTV.Core.Tests.Emby; + +[TestFixture] +public class EmbyPathReplacementServiceTests +{ + [Test] + public async Task EmbyWindows_To_EtvWindows() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"C:\Something\Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); + } + + [Test] + public async Task EmbyWindows_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"C:\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"C:\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task EmbyWindows_To_EtvLinux_UncPath() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"\\192.168.1.100\Something\Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task EmbyWindows_To_EtvLinux_UncPathWithTrailingSlash() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"\\192.168.1.100\Something\Some Shared Folder\", + LocalPath = @"/mnt/something else/Some Shared Folder/", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Windows" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"\\192.168.1.100\Something\Some Shared Folder\Some Movie\Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task EmbyLinux_To_EtvWindows() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"C:\Something Else\Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(true); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"C:\Something Else\Some Shared Folder\Some Movie\Some Movie.mkv"); + } + + [Test] + public async Task EmbyLinux_To_EtvLinux() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"/mnt/something/Some Shared Folder", + LocalPath = @"/mnt/something else/Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task Should_Not_Throw_For_Null_EmbyPath() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = null, + LocalPath = @"/mnt/something else/Some Shared Folder", + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task Should_Not_Throw_For_Null_LocalPath() + { + var replacements = new List + { + new() + { + Id = 1, + EmbyPath = @"/mnt/something/Some Shared Folder", + LocalPath = null, + EmbyMediaSource = new EmbyMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetEmbyPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new EmbyPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementEmbyPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/Some Movie/Some Movie.mkv"); + } +} \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs b/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs index 3e61e914e..539bba537 100644 --- a/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs +++ b/ErsatzTV.Core.Tests/Jellyfin/JellyfinPathReplacementServiceTests.cs @@ -204,4 +204,68 @@ public class JellyfinPathReplacementServiceTests result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); } + + [Test] + public async Task Should_Not_Throw_For_Null_JellyfinPath() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = null, + LocalPath = @"/mnt/something else/Some Shared Folder", + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task Should_Not_Throw_For_Null_LocalPath() + { + var replacements = new List + { + new() + { + Id = 1, + JellyfinPath = @"/mnt/something/Some Shared Folder", + LocalPath = null, + JellyfinMediaSource = new JellyfinMediaSource { OperatingSystem = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetJellyfinPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new JellyfinPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementJellyfinPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/Some Movie/Some Movie.mkv"); + } } \ No newline at end of file diff --git a/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs b/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs index 582a4f8c4..dd2364685 100644 --- a/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs +++ b/ErsatzTV.Core.Tests/Plex/PlexPathReplacementServiceTests.cs @@ -204,4 +204,68 @@ public class PlexPathReplacementServiceTests result.Should().Be(@"/mnt/something else/Some Shared Folder/Some Movie/Some Movie.mkv"); } + + [Test] + public async Task Should_Not_Throw_For_Null_PlexPath() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = null, + LocalPath = @"/mnt/something else/Some Shared Folder", + PlexMediaSource = new PlexMediaSource { Platform = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + } + + [Test] + public async Task Should_Not_Throw_For_Null_LocalPath() + { + var replacements = new List + { + new() + { + Id = 1, + PlexPath = @"/mnt/something/Some Shared Folder", + LocalPath = null, + PlexMediaSource = new PlexMediaSource { Platform = "Linux" } + } + }; + + var repo = new Mock(); + repo.Setup(x => x.GetPlexPathReplacementsByLibraryId(It.IsAny())).Returns(replacements.AsTask()); + + var runtime = new Mock(); + runtime.Setup(x => x.IsOSPlatform(OSPlatform.Windows)).Returns(false); + + var service = new PlexPathReplacementService( + repo.Object, + runtime.Object, + new Mock>().Object); + + string result = await service.GetReplacementPlexPath( + 0, + @"/mnt/something/Some Shared Folder/Some Movie/Some Movie.mkv"); + + result.Should().Be(@"/Some Movie/Some Movie.mkv"); + } } \ No newline at end of file diff --git a/ErsatzTV.Core/Emby/EmbyPathReplacementService.cs b/ErsatzTV.Core/Emby/EmbyPathReplacementService.cs index 443725283..14539c914 100644 --- a/ErsatzTV.Core/Emby/EmbyPathReplacementService.cs +++ b/ErsatzTV.Core/Emby/EmbyPathReplacementService.cs @@ -40,6 +40,11 @@ public class EmbyPathReplacementService : IEmbyPathReplacementService .SingleOrDefault( r => { + if (string.IsNullOrWhiteSpace(r.EmbyPath)) + { + return false; + } + string separatorChar = IsWindows(r.EmbyMediaSource, path) ? @"\" : @"/"; string prefix = r.EmbyPath.EndsWith(separatorChar) ? r.EmbyPath @@ -47,32 +52,32 @@ public class EmbyPathReplacementService : IEmbyPathReplacementService return path.StartsWith(prefix); }); - return maybeReplacement.Match( - replacement => + foreach (EmbyPathReplacement replacement in maybeReplacement) + { + string finalPath = path.Replace(replacement.EmbyPath, replacement.LocalPath); + if (IsWindows(replacement.EmbyMediaSource, path) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) { - string finalPath = path.Replace(replacement.EmbyPath, replacement.LocalPath); - if (IsWindows(replacement.EmbyMediaSource, path) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"\", @"/"); - } - else if (!IsWindows(replacement.EmbyMediaSource, path) && - _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"/", @"\"); - } + finalPath = finalPath.Replace(@"\", @"/"); + } + else if (!IsWindows(replacement.EmbyMediaSource, path) && + _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) + { + finalPath = finalPath.Replace(@"/", @"\"); + } - if (log) - { - _logger.LogInformation( - "Replacing emby path {EmbyPath} with {LocalPath} resulting in {FinalPath}", - replacement.EmbyPath, - replacement.LocalPath, - finalPath); - } + if (log) + { + _logger.LogInformation( + "Replacing emby path {EmbyPath} with {LocalPath} resulting in {FinalPath}", + replacement.EmbyPath, + replacement.LocalPath, + finalPath); + } - return finalPath; - }, - () => path); + return finalPath; + } + + return path; } private static bool IsWindows(EmbyMediaSource embyMediaSource, string path) diff --git a/ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs b/ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs index b1cca0dbb..0c5480e61 100644 --- a/ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs +++ b/ErsatzTV.Core/Jellyfin/JellyfinPathReplacementService.cs @@ -40,6 +40,11 @@ public class JellyfinPathReplacementService : IJellyfinPathReplacementService .SingleOrDefault( r => { + if (string.IsNullOrWhiteSpace(r.JellyfinPath)) + { + return false; + } + string separatorChar = IsWindows(r.JellyfinMediaSource, path) ? @"\" : @"/"; string prefix = r.JellyfinPath.EndsWith(separatorChar) ? r.JellyfinPath @@ -47,33 +52,33 @@ public class JellyfinPathReplacementService : IJellyfinPathReplacementService return path.StartsWith(prefix); }); - return maybeReplacement.Match( - replacement => + foreach (JellyfinPathReplacement replacement in maybeReplacement) + { + string finalPath = path.Replace(replacement.JellyfinPath, replacement.LocalPath); + if (IsWindows(replacement.JellyfinMediaSource, path) && + !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) { - string finalPath = path.Replace(replacement.JellyfinPath, replacement.LocalPath); - if (IsWindows(replacement.JellyfinMediaSource, path) && - !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"\", @"/"); - } - else if (!IsWindows(replacement.JellyfinMediaSource, path) && - _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"/", @"\"); - } + finalPath = finalPath.Replace(@"\", @"/"); + } + else if (!IsWindows(replacement.JellyfinMediaSource, path) && + _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) + { + finalPath = finalPath.Replace(@"/", @"\"); + } - if (log) - { - _logger.LogInformation( - "Replacing jellyfin path {JellyfinPath} with {LocalPath} resulting in {FinalPath}", - replacement.JellyfinPath, - replacement.LocalPath, - finalPath); - } + if (log) + { + _logger.LogInformation( + "Replacing jellyfin path {JellyfinPath} with {LocalPath} resulting in {FinalPath}", + replacement.JellyfinPath, + replacement.LocalPath, + finalPath); + } - return finalPath; - }, - () => path); + return finalPath; + } + + return path; } private static bool IsWindows(JellyfinMediaSource jellyfinMediaSource, string path) diff --git a/ErsatzTV.Core/Plex/PlexPathReplacementService.cs b/ErsatzTV.Core/Plex/PlexPathReplacementService.cs index 9dbce8c95..6be69c8e8 100644 --- a/ErsatzTV.Core/Plex/PlexPathReplacementService.cs +++ b/ErsatzTV.Core/Plex/PlexPathReplacementService.cs @@ -37,36 +37,41 @@ public class PlexPathReplacementService : IPlexPathReplacementService .SingleOrDefault( r => { + if (string.IsNullOrWhiteSpace(r.PlexPath)) + { + return false; + } + string separatorChar = IsWindows(r.PlexMediaSource) ? @"\" : @"/"; string prefix = r.PlexPath.EndsWith(separatorChar) ? r.PlexPath : r.PlexPath + separatorChar; return path.StartsWith(prefix); }); - return maybeReplacement.Match( - replacement => + foreach (PlexPathReplacement replacement in maybeReplacement) + { + string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath); + if (IsWindows(replacement.PlexMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) { - string finalPath = path.Replace(replacement.PlexPath, replacement.LocalPath); - if (IsWindows(replacement.PlexMediaSource) && !_runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"\", @"/"); - } - else if (!IsWindows(replacement.PlexMediaSource) && _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) - { - finalPath = finalPath.Replace(@"/", @"\"); - } + finalPath = finalPath.Replace(@"\", @"/"); + } + else if (!IsWindows(replacement.PlexMediaSource) && _runtimeInfo.IsOSPlatform(OSPlatform.Windows)) + { + finalPath = finalPath.Replace(@"/", @"\"); + } - if (log) - { - _logger.LogInformation( - "Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}", - replacement.PlexPath, - replacement.LocalPath, - finalPath); - } + if (log) + { + _logger.LogInformation( + "Replacing plex path {PlexPath} with {LocalPath} resulting in {FinalPath}", + replacement.PlexPath, + replacement.LocalPath, + finalPath); + } - return finalPath; - }, - () => path); + return finalPath; + } + + return path; } private static bool IsWindows(PlexMediaSource plexMediaSource) =>