From 7c1169d0dd145789d6703cf739574412efab6034 Mon Sep 17 00:00:00 2001 From: jwmann Date: Sat, 25 Apr 2026 13:04:40 -0400 Subject: [PATCH] Fix QBit Import Path for Single File Torrents --- .../Services/QBittorrentTest.cs | 56 +++++++++++++++++++ .../RdtClient.Service/Services/QBittorrent.cs | 34 ++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs index 6e74451..e050dee 100644 --- a/server/RdtClient.Service.Test/Services/QBittorrentTest.cs +++ b/server/RdtClient.Service.Test/Services/QBittorrentTest.cs @@ -1,5 +1,7 @@ using Microsoft.Extensions.Logging; using Moq; +using System.Text.Json; +using RdtClient.Data.Data; using RdtClient.Data.Enums; using RdtClient.Data.Models.Data; using RdtClient.Data.Models.DebridClient; @@ -208,4 +210,58 @@ public class QBittorrentTest Assert.NotNull(result); Assert.False(result!.IsPrivate); } + + [Fact] + public async Task TorrentInfo_ShouldUseSelectedTopLevelFileName_WhenRootFileAddsOnlyTheExtension() + { + // Arrange + var previousMappedPath = SettingData.Get.DownloadClient.MappedPath; + SettingData.Get.DownloadClient.MappedPath = "/data/downloads"; + + try + { + var torrentRootName = "Example.Series.S01E01.1080p.WEB-DL-GROUP"; + var selectedFileName = $"{torrentRootName}.mkv"; + + var torrent = new Torrent + { + Hash = "hash1", + Category = "tv", + Completed = DateTimeOffset.UtcNow, + RdName = torrentRootName, + RdFiles = JsonSerializer.Serialize(new List + { + new() + { + Id = 1, + Path = $"/{selectedFileName}", + Bytes = 1000, + Selected = true + }, + new() + { + Id = 2, + Path = $"/{torrentRootName}.nfo", + Bytes = 10, + Selected = false + } + }), + Type = DownloadType.Torrent + }; + + _torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List { torrent }); + + // Act + var result = await _qBittorrent.TorrentInfo(); + + // Assert + Assert.Single(result); + Assert.Equal(Path.Combine("/data/downloads", "tv", selectedFileName) + Path.DirectorySeparatorChar, result[0].ContentPath); + Assert.Equal(torrentRootName, result[0].Name); + } + finally + { + SettingData.Get.DownloadClient.MappedPath = previousMappedPath; + } + } } diff --git a/server/RdtClient.Service/Services/QBittorrent.cs b/server/RdtClient.Service/Services/QBittorrent.cs index a69b6a6..9f7d6cf 100644 --- a/server/RdtClient.Service/Services/QBittorrent.cs +++ b/server/RdtClient.Service/Services/QBittorrent.cs @@ -204,6 +204,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent } var torrentPath = downloadPath; + var contentPathName = GetContentPathName(torrent); if (!String.IsNullOrWhiteSpace(torrent.RdName)) { @@ -214,7 +215,7 @@ public class QBittorrent(ILogger logger, Settings settings, Authent } else { - torrentPath = Path.Combine(downloadPath, torrent.RdName) + Path.DirectorySeparatorChar; + torrentPath = Path.Combine(downloadPath, contentPathName) + Path.DirectorySeparatorChar; } } @@ -328,6 +329,37 @@ public class QBittorrent(ILogger logger, Settings settings, Authent return results; } + private static String GetContentPathName(Torrent torrent) + { + if (String.IsNullOrWhiteSpace(torrent.RdName)) + { + return torrent.RdName ?? String.Empty; + } + + var topLevelSelectedFiles = torrent.Files + .Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path)) + .Select(m => m.Path.Trim('/').Trim('\\')) + .Where(m => m.IndexOfAny(['/', '\\']) < 0) + .Select(Path.GetFileName) + .Where(m => !String.IsNullOrWhiteSpace(m)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (topLevelSelectedFiles.Count == 1) + { + var selectedFileName = topLevelSelectedFiles[0]!; + var selectedFileBaseName = Path.GetFileNameWithoutExtension(selectedFileName); + + if (!String.IsNullOrWhiteSpace(selectedFileBaseName) && + selectedFileBaseName.Equals(torrent.RdName, StringComparison.OrdinalIgnoreCase)) + { + return selectedFileName; + } + } + + return torrent.RdName; + } + public async Task?> TorrentFileContents(String hash) { var torrent = await torrents.GetByHash(hash);