Add additional compatibility when a Torrent stores its media file within a Sub Folder
This commit is contained in:
parent
7c1169d0dd
commit
cd1310c1e9
2 changed files with 259 additions and 3 deletions
|
|
@ -220,7 +220,7 @@ public class QBittorrentTest
|
|||
|
||||
try
|
||||
{
|
||||
var torrentRootName = "Example.Series.S01E01.1080p.WEB-DL-GROUP";
|
||||
var torrentRootName = "Sample.Show.S01E01.1080p.WEB-DL-GROUP";
|
||||
var selectedFileName = $"{torrentRootName}.mkv";
|
||||
|
||||
var torrent = new Torrent
|
||||
|
|
@ -264,4 +264,131 @@ public class QBittorrentTest
|
|||
SettingData.Get.DownloadClient.MappedPath = previousMappedPath;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSingleFileDirectoryDiffersFromRdName()
|
||||
{
|
||||
var previousMappedPath = SettingData.Get.DownloadClient.MappedPath;
|
||||
var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
SettingData.Get.DownloadClient.MappedPath = mappedPath;
|
||||
|
||||
try
|
||||
{
|
||||
var category = "tv";
|
||||
var selectedFileName = "Sample.Show.S01E02.1080p.WEB-DL-GROUP.mkv";
|
||||
var contentRoot = Path.Combine(mappedPath, category, selectedFileName);
|
||||
Directory.CreateDirectory(contentRoot);
|
||||
await File.WriteAllTextAsync(Path.Combine(contentRoot, selectedFileName), "test");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Category = category,
|
||||
Completed = DateTimeOffset.UtcNow,
|
||||
Downloads = new List<Download>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DownloadId = Guid.NewGuid(),
|
||||
FileName = selectedFileName,
|
||||
Link = "https://fake.url/Sample.Show.S01E02.1080p.WEB-DL-GROUP.mkv",
|
||||
Completed = DateTimeOffset.UtcNow
|
||||
}
|
||||
},
|
||||
RdName = "tracker.example - Sample.Show.S01E02.1080p.WEB-DL-GROUP",
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = $"/{selectedFileName}",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
}
|
||||
}),
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent> { torrent });
|
||||
|
||||
var result = await _qBittorrent.TorrentInfo();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(contentRoot + Path.DirectorySeparatorChar, result[0].ContentPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SettingData.Get.DownloadClient.MappedPath = previousMappedPath;
|
||||
|
||||
if (Directory.Exists(mappedPath))
|
||||
{
|
||||
Directory.Delete(mappedPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TorrentInfo_ShouldUseExistingCompletedContentRoot_WhenSelectedFileIsNestedUnderDifferentRoot()
|
||||
{
|
||||
var previousMappedPath = SettingData.Get.DownloadClient.MappedPath;
|
||||
var mappedPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||
SettingData.Get.DownloadClient.MappedPath = mappedPath;
|
||||
|
||||
try
|
||||
{
|
||||
var category = "tv";
|
||||
var rootDirectoryName = "tracker.example - Sample.Show.S01E03.1080p.x265-GROUP.mkv";
|
||||
var nestedDirectoryName = "Sample.Show.S01E03.1080p.x265-GROUP";
|
||||
var fileName = "Sample.Show.S01E03.1080p.x265-GROUP.mkv";
|
||||
var contentRoot = Path.Combine(mappedPath, category, rootDirectoryName);
|
||||
var nestedDirectory = Path.Combine(contentRoot, nestedDirectoryName);
|
||||
Directory.CreateDirectory(nestedDirectory);
|
||||
await File.WriteAllTextAsync(Path.Combine(nestedDirectory, fileName), "test");
|
||||
|
||||
var torrent = new Torrent
|
||||
{
|
||||
Hash = "hash1",
|
||||
Category = category,
|
||||
Completed = DateTimeOffset.UtcNow,
|
||||
Downloads = new List<Download>
|
||||
{
|
||||
new()
|
||||
{
|
||||
DownloadId = Guid.NewGuid(),
|
||||
FileName = fileName,
|
||||
Link = "https://fake.url/Sample.Show.S01E03.1080p.x265-GROUP.mkv",
|
||||
Completed = DateTimeOffset.UtcNow
|
||||
}
|
||||
},
|
||||
RdName = "tracker.example - Sample.Show.S01E03.1080p.x265-GROUP",
|
||||
RdFiles = JsonSerializer.Serialize(new List<DebridClientFile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Id = 1,
|
||||
Path = $"/{nestedDirectoryName}/{fileName}",
|
||||
Bytes = 1000,
|
||||
Selected = true
|
||||
}
|
||||
}),
|
||||
Type = DownloadType.Torrent
|
||||
};
|
||||
|
||||
_torrentsMock.Setup(m => m.Get()).ReturnsAsync(new List<Torrent> { torrent });
|
||||
|
||||
var result = await _qBittorrent.TorrentInfo();
|
||||
|
||||
Assert.Single(result);
|
||||
Assert.Equal(contentRoot + Path.DirectorySeparatorChar, result[0].ContentPath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SettingData.Get.DownloadClient.MappedPath = previousMappedPath;
|
||||
|
||||
if (Directory.Exists(mappedPath))
|
||||
{
|
||||
Directory.Delete(mappedPath, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -204,7 +204,6 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
}
|
||||
|
||||
var torrentPath = downloadPath;
|
||||
var contentPathName = GetContentPathName(torrent);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(torrent.RdName))
|
||||
{
|
||||
|
|
@ -215,7 +214,17 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
}
|
||||
else
|
||||
{
|
||||
torrentPath = Path.Combine(downloadPath, contentPathName) + Path.DirectorySeparatorChar;
|
||||
var existingContentPath = GetExistingContentPath(downloadPath, torrent);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(existingContentPath))
|
||||
{
|
||||
torrentPath = existingContentPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
var contentPathName = GetContentPathName(torrent);
|
||||
torrentPath = Path.Combine(downloadPath, contentPathName) + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -360,6 +369,126 @@ public class QBittorrent(ILogger<QBittorrent> logger, Settings settings, Authent
|
|||
return torrent.RdName;
|
||||
}
|
||||
|
||||
private static String? GetExistingContentPath(String downloadPath, Torrent torrent)
|
||||
{
|
||||
if (!torrent.Completed.HasValue || !Directory.Exists(downloadPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var selectedFilePaths = torrent.Files
|
||||
.Where(m => m.Selected && !String.IsNullOrWhiteSpace(m.Path))
|
||||
.Select(m => NormalizeRelativePath(m.Path!))
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
var downloadFileNames = torrent.Downloads
|
||||
.Select(m => m.FileName)
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Select(m => Path.GetFileName(m!))
|
||||
.Where(m => !String.IsNullOrWhiteSpace(m))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
|
||||
if (selectedFilePaths.Count == 0 && downloadFileNames.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidateRoot in GetCandidateContentRoots(downloadPath, torrent, selectedFilePaths, downloadFileNames))
|
||||
{
|
||||
if (IsMatchingContentRoot(candidateRoot, selectedFilePaths, downloadFileNames))
|
||||
{
|
||||
return candidateRoot + Path.DirectorySeparatorChar;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static IEnumerable<String> GetCandidateContentRoots(String downloadPath,
|
||||
Torrent torrent,
|
||||
IEnumerable<String> selectedFilePaths,
|
||||
IEnumerable<String> downloadFileNames)
|
||||
{
|
||||
var yielded = new HashSet<String>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
void AddCandidate(ICollection<String> candidates, String? name)
|
||||
{
|
||||
if (!String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
candidates.Add(Path.Combine(downloadPath, name));
|
||||
}
|
||||
}
|
||||
|
||||
var directCandidates = new List<String>();
|
||||
|
||||
AddCandidate(directCandidates, torrent.RdName);
|
||||
|
||||
foreach (var fileName in downloadFileNames)
|
||||
{
|
||||
AddCandidate(directCandidates, fileName);
|
||||
}
|
||||
|
||||
foreach (var selectedFilePath in selectedFilePaths)
|
||||
{
|
||||
AddCandidate(directCandidates, Path.GetFileName(selectedFilePath));
|
||||
AddCandidate(directCandidates, GetFirstPathComponent(selectedFilePath));
|
||||
}
|
||||
|
||||
foreach (var candidate in directCandidates)
|
||||
{
|
||||
if (Directory.Exists(candidate) && yielded.Add(candidate))
|
||||
{
|
||||
yield return candidate;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var directory in Directory.EnumerateDirectories(downloadPath))
|
||||
{
|
||||
if (yielded.Add(directory))
|
||||
{
|
||||
yield return directory;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Boolean IsMatchingContentRoot(String candidateRoot,
|
||||
IEnumerable<String> selectedFilePaths,
|
||||
IEnumerable<String> downloadFileNames)
|
||||
{
|
||||
foreach (var selectedFilePath in selectedFilePaths)
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidateRoot, selectedFilePath)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var fileName in downloadFileNames)
|
||||
{
|
||||
if (File.Exists(Path.Combine(candidateRoot, fileName)))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String NormalizeRelativePath(String path)
|
||||
{
|
||||
return path.Trim('/').Trim('\\').Replace('\\', Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
private static String GetFirstPathComponent(String path)
|
||||
{
|
||||
var separatorIndex = path.IndexOfAny(['/', '\\']);
|
||||
|
||||
return separatorIndex < 0 ? path : path[..separatorIndex];
|
||||
}
|
||||
|
||||
public async Task<IList<TorrentFileItem>?> TorrentFileContents(String hash)
|
||||
{
|
||||
var torrent = await torrents.GetByHash(hash);
|
||||
|
|
|
|||
Loading…
Reference in a new issue