mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-18 16:29:41 +02:00
Fixed: Get actual file names from QBittorrent API (#5226)
* Fixed: Get actual file names from QBittorrent API Previously we were assuming that the output filename was the category output dir + the torrent name. This isn't true if the torrent has been renamed or sanitized.
This commit is contained in:
parent
b7719662a7
commit
3e795d290b
@ -37,6 +37,7 @@ public void Setup()
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(c => c.State = TrackedDownloadState.Downloading)
|
||||
.With(c => c.ImportItem = completed)
|
||||
.With(c => c.DownloadItem = completed)
|
||||
.With(c => c.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
@ -47,6 +47,10 @@ public void Setup()
|
||||
.Setup(c => c.Get(It.IsAny<int>()))
|
||||
.Returns(Mocker.GetMock<IDownloadClient>().Object);
|
||||
|
||||
Mocker.GetMock<IProvideImportItemService>()
|
||||
.Setup(c => c.ProvideImportItem(It.IsAny<DownloadClientItem>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns((DownloadClientItem item, DownloadClientItem previous) => item);
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
|
||||
.Returns(new MovieHistory());
|
||||
|
@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.Clients.QBittorrent;
|
||||
@ -124,6 +126,24 @@ protected virtual void GivenTorrents(List<QBittorrentTorrent> torrents)
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetTorrents(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(torrents);
|
||||
|
||||
foreach (var torrent in torrents)
|
||||
{
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetTorrentProperties(torrent.Hash.ToLower(), It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new QBittorrentTorrentProperties { SavePath = torrent.SavePath });
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetTorrentFiles(torrent.Hash.ToLower(), It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new List<QBittorrentTorrentFile> { new QBittorrentTorrentFile { Name = torrent.Name } });
|
||||
}
|
||||
}
|
||||
|
||||
private void GivenTorrentFiles(string hash, List<QBittorrentTorrentFile> files)
|
||||
{
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(s => s.GetTorrentFiles(hash.ToLower(), It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(files);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -259,6 +279,78 @@ public void stalledDL_item_should_have_required_properties()
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void single_file_torrent_outputpath_should_have_sanitised_name()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = @"Droned.S01E01.Test\'s.1080p.WEB-DL-DRONE.mkv",
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "stalledDL",
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic()
|
||||
};
|
||||
|
||||
var file = new QBittorrentTorrentFile
|
||||
{
|
||||
Name = "Droned.S01E01.Tests.1080p.WEB-DL-DRONE.mkv"
|
||||
};
|
||||
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
GivenTorrentFiles(torrent.Hash, new List<QBittorrentTorrentFile> { file });
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadId = torrent.Hash
|
||||
};
|
||||
|
||||
var result = Subject.GetImportItem(item, null);
|
||||
|
||||
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, file.Name));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void multi_file_torrent_outputpath_should_have_sanitised_name()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = @"Droned.S01.\1/2",
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "stalledDL",
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic()
|
||||
};
|
||||
|
||||
var files = new List<QBittorrentTorrentFile>
|
||||
{
|
||||
new QBittorrentTorrentFile
|
||||
{
|
||||
Name = @"Droned.S01.12\E01.mkv".AsOsAgnostic()
|
||||
},
|
||||
new QBittorrentTorrentFile
|
||||
{
|
||||
Name = @"Droned.S01.12\E02.mkv".AsOsAgnostic()
|
||||
}
|
||||
};
|
||||
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
GivenTorrentFiles(torrent.Hash, files);
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadId = torrent.Hash
|
||||
};
|
||||
|
||||
var result = Subject.GetImportItem(item, null);
|
||||
|
||||
result.OutputPath.FullPath.Should().Be(Path.Combine(torrent.SavePath, "Droned.S01.12"));
|
||||
}
|
||||
|
||||
public void missingFiles_item_should_have_required_properties()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
@ -279,6 +371,39 @@ public void missingFiles_item_should_have_required_properties()
|
||||
item.RemainingTime.Should().NotHaveValue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void api_261_should_use_content_path()
|
||||
{
|
||||
var torrent = new QBittorrentTorrent
|
||||
{
|
||||
Hash = "HASH",
|
||||
Name = @"Droned.S01.\1/2",
|
||||
Size = 1000,
|
||||
Progress = 0.7,
|
||||
Eta = 8640000,
|
||||
State = "stalledDL",
|
||||
Label = "",
|
||||
SavePath = @"C:\Torrents".AsOsAgnostic(),
|
||||
ContentPath = @"C:\Torrents\Droned.S01.12".AsOsAgnostic()
|
||||
};
|
||||
|
||||
GivenTorrents(new List<QBittorrentTorrent> { torrent });
|
||||
|
||||
Mocker.GetMock<IQBittorrentProxy>()
|
||||
.Setup(v => v.GetApiVersion(It.IsAny<QBittorrentSettings>()))
|
||||
.Returns(new Version(2, 6, 1));
|
||||
|
||||
var item = new DownloadClientItem
|
||||
{
|
||||
DownloadId = torrent.Hash,
|
||||
OutputPath = new OsPath(torrent.ContentPath)
|
||||
};
|
||||
|
||||
var result = Subject.GetImportItem(item, null);
|
||||
|
||||
result.OutputPath.FullPath.Should().Be(torrent.ContentPath);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Download_should_return_unique_id()
|
||||
{
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using FluentValidation.Results;
|
||||
using NLog;
|
||||
@ -122,6 +123,7 @@ protected override string AddFromTorrentFile(RemoteMovie remoteMovie, string has
|
||||
|
||||
public override IEnumerable<DownloadClientItem> GetItems()
|
||||
{
|
||||
var version = Proxy.GetApiVersion(Settings);
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
var torrents = Proxy.GetTorrents(Settings);
|
||||
|
||||
@ -138,19 +140,18 @@ public override IEnumerable<DownloadClientItem> GetItems()
|
||||
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
|
||||
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
|
||||
RemainingTime = GetRemainingTime(torrent),
|
||||
SeedRatio = torrent.Ratio,
|
||||
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.SavePath)),
|
||||
SeedRatio = torrent.Ratio
|
||||
};
|
||||
|
||||
if (version >= new Version("2.6.1"))
|
||||
{
|
||||
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.ContentPath));
|
||||
}
|
||||
|
||||
// Avoid removing torrents that haven't reached the global max ratio.
|
||||
// Removal also requires the torrent to be paused, in case a higher max ratio was set on the torrent itself (which is not exposed by the api).
|
||||
item.CanMoveFiles = item.CanBeRemoved = torrent.State == "pausedUP" && HasReachedSeedLimit(torrent, config);
|
||||
|
||||
if (!item.OutputPath.IsEmpty && item.OutputPath.FileName != torrent.Name)
|
||||
{
|
||||
item.OutputPath += torrent.Name;
|
||||
}
|
||||
|
||||
switch (torrent.State)
|
||||
{
|
||||
case "error": // some error occurred, applies to paused torrents
|
||||
@ -224,6 +225,49 @@ public override void RemoveItem(string hash, bool deleteData)
|
||||
Proxy.RemoveTorrent(hash.ToLower(), deleteData, Settings);
|
||||
}
|
||||
|
||||
public override DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt)
|
||||
{
|
||||
// On API version >= 2.6.1 this is already set correctly
|
||||
if (!item.OutputPath.IsEmpty)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
var files = Proxy.GetTorrentFiles(item.DownloadId.ToLower(), Settings);
|
||||
if (!files.Any())
|
||||
{
|
||||
_logger.Debug($"No files found for torrent {item.Title} in qBittorrent");
|
||||
return item;
|
||||
}
|
||||
|
||||
var properties = Proxy.GetTorrentProperties(item.DownloadId.ToLower(), Settings);
|
||||
var savePath = new OsPath(properties.SavePath);
|
||||
|
||||
var result = item.Clone();
|
||||
|
||||
OsPath outputPath;
|
||||
if (files.Count == 1)
|
||||
{
|
||||
outputPath = savePath + files[0].Name;
|
||||
}
|
||||
else
|
||||
{
|
||||
// we have multiple files in the torrent so just get
|
||||
// the first subdirectory
|
||||
var relativePath = new OsPath(files[0].Name);
|
||||
while (!relativePath.Directory.IsEmpty)
|
||||
{
|
||||
relativePath = relativePath.Directory;
|
||||
}
|
||||
|
||||
outputPath = savePath + relativePath.FileName;
|
||||
}
|
||||
|
||||
result.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override DownloadClientInfo GetStatus()
|
||||
{
|
||||
var config = Proxy.GetConfig(Settings);
|
||||
|
@ -16,6 +16,7 @@ public interface IQBittorrentProxy
|
||||
QBittorrentPreferences GetConfig(QBittorrentSettings settings);
|
||||
List<QBittorrentTorrent> GetTorrents(QBittorrentSettings settings);
|
||||
QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorrentSettings settings);
|
||||
List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings);
|
||||
|
||||
void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings);
|
||||
void AddTorrentFromFile(string fileName, byte[] fileContent, QBittorrentSettings settings);
|
||||
|
@ -105,6 +105,14 @@ public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorren
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource($"/query/propertiesFiles/{hash}");
|
||||
var response = ProcessRequest<List<QBittorrentTorrentFile>>(request, settings);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/command/download")
|
||||
|
@ -110,6 +110,15 @@ public QBittorrentTorrentProperties GetTorrentProperties(string hash, QBittorren
|
||||
return response;
|
||||
}
|
||||
|
||||
public List<QBittorrentTorrentFile> GetTorrentFiles(string hash, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/files")
|
||||
.AddQueryParam("hash", hash);
|
||||
var response = ProcessRequest<List<QBittorrentTorrentFile>>(request, settings);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
public void AddTorrentFromUrl(string torrentUrl, QBittorrentSettings settings)
|
||||
{
|
||||
var request = BuildRequest(settings).Resource("/api/v2/torrents/add")
|
||||
|
@ -24,6 +24,9 @@ public class QBittorrentTorrent
|
||||
[JsonProperty(PropertyName = "save_path")]
|
||||
public string SavePath { get; set; } // Torrent save path
|
||||
|
||||
[JsonProperty(PropertyName = "content_path")]
|
||||
public string ContentPath { get; set; } // Torrent save path
|
||||
|
||||
public float Ratio { get; set; } // Torrent share ratio
|
||||
|
||||
[JsonProperty(PropertyName = "ratio_limit")] // Per torrent seeding ratio limit (-2 = use global, -1 = unlimited)
|
||||
@ -40,7 +43,15 @@ public class QBittorrentTorrentProperties
|
||||
{
|
||||
public string Hash { get; set; } // Torrent hash
|
||||
|
||||
[JsonProperty(PropertyName = "save_path")]
|
||||
public string SavePath { get; set; }
|
||||
|
||||
[JsonProperty(PropertyName = "seeding_time")]
|
||||
public long SeedingTime { get; set; } // Torrent seeding time
|
||||
}
|
||||
|
||||
public class QBittorrentTorrentFile
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
||||
}
|
||||
|
@ -27,6 +27,7 @@ public class CompletedDownloadService : ICompletedDownloadService
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IProvideImportItemService _importItemService;
|
||||
private readonly IDownloadedMovieImportService _downloadedMovieImportService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IMovieService _movieService;
|
||||
@ -35,6 +36,7 @@ public class CompletedDownloadService : ICompletedDownloadService
|
||||
|
||||
public CompletedDownloadService(IEventAggregator eventAggregator,
|
||||
IHistoryService historyService,
|
||||
IProvideImportItemService importItemService,
|
||||
IDownloadedMovieImportService downloadedMovieImportService,
|
||||
IParsingService parsingService,
|
||||
IMovieService movieService,
|
||||
@ -43,6 +45,7 @@ public CompletedDownloadService(IEventAggregator eventAggregator,
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_historyService = historyService;
|
||||
_importItemService = importItemService;
|
||||
_downloadedMovieImportService = downloadedMovieImportService;
|
||||
_parsingService = parsingService;
|
||||
_movieService = movieService;
|
||||
@ -57,6 +60,8 @@ public void Check(TrackedDownload trackedDownload)
|
||||
return;
|
||||
}
|
||||
|
||||
trackedDownload.ImportItem = _importItemService.ProvideImportItem(trackedDownload.DownloadItem, trackedDownload.ImportItem);
|
||||
|
||||
// Only process tracked downloads that are still downloading
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading)
|
||||
{
|
||||
@ -71,7 +76,7 @@ public void Check(TrackedDownload trackedDownload)
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
|
||||
var downloadItemOutputPath = trackedDownload.ImportItem.OutputPath;
|
||||
|
||||
if (downloadItemOutputPath.IsEmpty)
|
||||
{
|
||||
@ -109,7 +114,7 @@ public void Import(TrackedDownload trackedDownload)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadState.Importing;
|
||||
|
||||
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
|
||||
var outputPath = trackedDownload.ImportItem.OutputPath.FullPath;
|
||||
|
||||
if (trackedDownload.RemoteMovie?.Movie == null)
|
||||
{
|
||||
@ -183,7 +188,7 @@ public bool VerifyImport(TrackedDownload trackedDownload, List<ImportResult> imp
|
||||
.Property("MovieId", trackedDownload.RemoteMovie.Movie.Id)
|
||||
.Property("DownloadId", trackedDownload.DownloadItem.DownloadId)
|
||||
.Property("Title", trackedDownload.DownloadItem.Title)
|
||||
.Property("Path", trackedDownload.DownloadItem.OutputPath.ToString())
|
||||
.Property("Path", trackedDownload.ImportItem.OutputPath.ToString())
|
||||
.WriteSentryWarn("DownloadHistoryIncomplete")
|
||||
.Write();
|
||||
}
|
||||
|
@ -66,6 +66,12 @@ public abstract DownloadProtocol Protocol
|
||||
|
||||
public abstract string Download(RemoteMovie remoteMovie);
|
||||
public abstract IEnumerable<DownloadClientItem> GetItems();
|
||||
|
||||
public virtual DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt)
|
||||
{
|
||||
return item;
|
||||
}
|
||||
|
||||
public abstract void RemoveItem(string downloadId, bool deleteData);
|
||||
public abstract DownloadClientInfo GetStatus();
|
||||
|
||||
|
@ -25,6 +25,11 @@ public class DownloadClientItem
|
||||
public bool CanMoveFiles { get; set; }
|
||||
public bool CanBeRemoved { get; set; }
|
||||
public bool Removed { get; set; }
|
||||
|
||||
public DownloadClientItem Clone()
|
||||
{
|
||||
return MemberwiseClone() as DownloadClientItem;
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadClientItemClientInfo
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
@ -10,6 +11,7 @@ public interface IDownloadClient : IProvider
|
||||
DownloadProtocol Protocol { get; }
|
||||
string Download(RemoteMovie remoteMovie);
|
||||
IEnumerable<DownloadClientItem> GetItems();
|
||||
DownloadClientItem GetImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt);
|
||||
void RemoveItem(string downloadId, bool deleteData);
|
||||
DownloadClientInfo GetStatus();
|
||||
void MarkItemAsImported(DownloadClientItem downloadClientItem);
|
||||
|
24
src/NzbDrone.Core/Download/PrepareImportService.cs
Normal file
24
src/NzbDrone.Core/Download/PrepareImportService.cs
Normal file
@ -0,0 +1,24 @@
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface IProvideImportItemService
|
||||
{
|
||||
DownloadClientItem ProvideImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt);
|
||||
}
|
||||
|
||||
public class ProvideImportItemService : IProvideImportItemService
|
||||
{
|
||||
private readonly IProvideDownloadClient _downloadClientProvider;
|
||||
|
||||
public ProvideImportItemService(IProvideDownloadClient downloadClientProvider)
|
||||
{
|
||||
_downloadClientProvider = downloadClientProvider;
|
||||
}
|
||||
|
||||
public DownloadClientItem ProvideImportItem(DownloadClientItem item, DownloadClientItem previousImportAttempt)
|
||||
{
|
||||
var client = _downloadClientProvider.Get(item.DownloadClientInfo.Id);
|
||||
|
||||
return client.GetImportItem(item, previousImportAttempt);
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ public class TrackedDownload
|
||||
{
|
||||
public int DownloadClient { get; set; }
|
||||
public DownloadClientItem DownloadItem { get; set; }
|
||||
public DownloadClientItem ImportItem { get; set; }
|
||||
public TrackedDownloadState State { get; set; }
|
||||
public TrackedDownloadStatus Status { get; private set; }
|
||||
public RemoteMovie RemoteMovie { get; set; }
|
||||
|
@ -73,7 +73,7 @@ public List<ManualImportItem> GetMediaFiles(string path, string downloadId, int?
|
||||
return new List<ManualImportItem>();
|
||||
}
|
||||
|
||||
path = trackedDownload.DownloadItem.OutputPath.FullPath;
|
||||
path = trackedDownload.ImportItem.OutputPath.FullPath;
|
||||
}
|
||||
|
||||
if (!_diskProvider.FolderExists(path))
|
||||
@ -305,14 +305,15 @@ public void Execute(ManualImportCommand message)
|
||||
var trackedDownload = groupedTrackedDownload.First().TrackedDownload;
|
||||
|
||||
var importMovie = groupedTrackedDownload.First().ImportResult.ImportDecision.LocalMovie.Movie;
|
||||
var outputPath = trackedDownload.ImportItem.OutputPath.FullPath;
|
||||
|
||||
if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath))
|
||||
if (_diskProvider.FolderExists(outputPath))
|
||||
{
|
||||
if (_downloadedMovieImportService.ShouldDeleteFolder(
|
||||
new DirectoryInfo(trackedDownload.DownloadItem.OutputPath.FullPath),
|
||||
new DirectoryInfo(outputPath),
|
||||
importMovie) && trackedDownload.DownloadItem.CanMoveFiles)
|
||||
{
|
||||
_diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true);
|
||||
_diskProvider.DeleteFolder(outputPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user