diff --git a/src/NzbDrone.Api/Indexers/ReleaseModule.cs b/src/NzbDrone.Api/Indexers/ReleaseModule.cs index 79588c582..7f92215fb 100644 --- a/src/NzbDrone.Api/Indexers/ReleaseModule.cs +++ b/src/NzbDrone.Api/Indexers/ReleaseModule.cs @@ -26,6 +26,7 @@ public class ReleaseModule : ReleaseModuleBase private readonly Logger _logger; private readonly ICached _remoteEpisodeCache; + private readonly ICached _remoteMovieCache; public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, ISearchForNzb nzbSearchService, @@ -49,6 +50,7 @@ public ReleaseModule(IFetchAndParseRss rssFetcherAndParser, PostValidator.RuleFor(s => s.Guid).NotEmpty(); _remoteEpisodeCache = cacheManager.GetCache(GetType(), "remoteEpisodes"); + _remoteMovieCache = cacheManager.GetCache(GetType(), "remoteMovies"); } private Response DownloadRelease(ReleaseResource release) @@ -59,7 +61,26 @@ private Response DownloadRelease(ReleaseResource release) { _logger.Debug("Couldn't find requested release in cache, cache timeout probably expired."); - return new NotFoundResponse(); + var remoteMovie = _remoteMovieCache.Find(release.Guid); + + if (remoteMovie == null) + { + return new NotFoundResponse(); + } + + try + { + _downloadService.DownloadReport(remoteMovie); + } + catch (ReleaseDownloadException ex) + { + _logger.Error(ex, ex.Message); + throw new NzbDroneClientException(HttpStatusCode.Conflict, "Getting release from indexer failed"); + } + + return release.AsResponse(); + + } try @@ -139,7 +160,15 @@ private List GetRss() protected override ReleaseResource MapDecision(DownloadDecision decision, int initialWeight) { - _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + if (decision.IsForMovie) + { + _remoteMovieCache.Set(decision.RemoteMovie.Release.Guid, decision.RemoteMovie, TimeSpan.FromMinutes(30)); + } + else + { + _remoteEpisodeCache.Set(decision.RemoteEpisode.Release.Guid, decision.RemoteEpisode, TimeSpan.FromMinutes(30)); + } + return base.MapDecision(decision, initialWeight); } } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index 5eab58b3b..299a56b13 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -58,6 +58,32 @@ public override string Download(RemoteEpisode remoteEpisode) return GetDownloadClientId(strmFile); } + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var title = remoteEpisode.Release.Title; + + if (remoteEpisode.ParsedEpisodeInfo.FullSeason) + { + throw new NotSupportedException("Full season releases are not supported with Pneumatic."); + } + + title = FileNameBuilder.CleanFileName(title); + + //Save to the Pneumatic directory (The user will need to ensure its accessible by XBMC) + var nzbFile = Path.Combine(Settings.NzbFolder, title + ".nzb"); + + _logger.Debug("Downloading NZB from: {0} to: {1}", url, nzbFile); + _httpClient.DownloadFile(url, nzbFile); + + _logger.Debug("NZB Download succeeded, saved to: {0}", nzbFile); + + var strmFile = WriteStrmFile(title, nzbFile); + + + return GetDownloadClientId(strmFile); + } + public bool IsConfigured => !string.IsNullOrWhiteSpace(Settings.NzbFolder); public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs index 5840526d7..d8c7e825a 100644 --- a/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/QBittorrent/QBittorrent.cs @@ -40,7 +40,7 @@ protected override string AddFromMagnetLink(RemoteEpisode remoteEpisode, string _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); } - var isRecentEpisode = true;//remoteEpisode.IsRecentEpisode(); TODO: Update to use RemoteMovie! + var isRecentEpisode = remoteEpisode.IsRecentEpisode(); if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) @@ -71,6 +71,46 @@ protected override string AddFromTorrentFile(RemoteEpisode remoteEpisode, string return hash; } + protected override string AddFromMagnetLink(RemoteMovie remoteEpisode, string hash, string magnetLink) + { + _proxy.AddTorrentFromUrl(magnetLink, Settings); + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + } + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + }*/ //TODO: Maybe reimplement for movies + + return hash; + } + + protected override string AddFromTorrentFile(RemoteMovie remoteEpisode, string hash, string filename, Byte[] fileContent) + { + _proxy.AddTorrentFromFile(filename, fileContent, Settings); + + if (Settings.TvCategory.IsNotNullOrWhiteSpace()) + { + _proxy.SetTorrentLabel(hash.ToLower(), Settings.TvCategory, Settings); + } + + /*var isRecentEpisode = remoteEpisode.IsRecentEpisode(); + + if (isRecentEpisode && Settings.RecentTvPriority == (int)QBittorrentPriority.First || + !isRecentEpisode && Settings.OlderTvPriority == (int)QBittorrentPriority.First) + { + _proxy.MoveTorrentToTopInQueue(hash.ToLower(), Settings); + }*/ + + return hash; + } + public override string Name => "qBittorrent"; public override IEnumerable GetItems() diff --git a/src/NzbDrone.Core/Download/DownloadClientBase.cs b/src/NzbDrone.Core/Download/DownloadClientBase.cs index 98ade2a69..0e48207ba 100644 --- a/src/NzbDrone.Core/Download/DownloadClientBase.cs +++ b/src/NzbDrone.Core/Download/DownloadClientBase.cs @@ -57,6 +57,7 @@ public abstract DownloadProtocol Protocol get; } + public abstract string Download(RemoteEpisode remoteEpisode); public abstract IEnumerable GetItems(); public abstract void RemoveItem(string downloadId, bool deleteData); @@ -147,5 +148,7 @@ protected ValidationFailure TestFolder(string folder, string propertyName, bool return null; } + + public abstract string Download(RemoteMovie remoteMovie); } } diff --git a/src/NzbDrone.Core/Download/DownloadService.cs b/src/NzbDrone.Core/Download/DownloadService.cs index c5ad3f770..6d0963199 100644 --- a/src/NzbDrone.Core/Download/DownloadService.cs +++ b/src/NzbDrone.Core/Download/DownloadService.cs @@ -15,6 +15,7 @@ namespace NzbDrone.Core.Download public interface IDownloadService { void DownloadReport(RemoteEpisode remoteEpisode); + void DownloadReport(RemoteMovie remoteMovie); } @@ -91,5 +92,62 @@ public void DownloadReport(RemoteEpisode remoteEpisode) _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); _eventAggregator.PublishEvent(episodeGrabbedEvent); } + + public void DownloadReport(RemoteMovie remoteMovie) + { + //Ensure.That(remoteEpisode.Series, () => remoteEpisode.Series).IsNotNull(); + //Ensure.That(remoteEpisode.Episodes, () => remoteEpisode.Episodes).HasItems(); TODO update this shit + + var downloadTitle = remoteMovie.Release.Title; + var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol); + + if (downloadClient == null) + { + _logger.Warn("{0} Download client isn't configured yet.", remoteMovie.Release.DownloadProtocol); + return; + } + + // Limit grabs to 2 per second. + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && !remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + var url = new HttpUri(remoteMovie.Release.DownloadUrl); + _rateLimitService.WaitAndPulse(url.Host, TimeSpan.FromSeconds(2)); + } + + string downloadClientId = ""; + try + { + downloadClientId = downloadClient.Download(remoteMovie); + _indexerStatusService.RecordSuccess(remoteMovie.Release.IndexerId); + } + catch (NotImplementedException ex) + { + _logger.Error(ex, "The download client you are using is currently not configured to download movies. Please choose another one."); + } + catch (ReleaseDownloadException ex) + { + var http429 = ex.InnerException as TooManyRequestsException; + if (http429 != null) + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId, http429.RetryAfter); + } + else + { + _indexerStatusService.RecordFailure(remoteMovie.Release.IndexerId); + } + throw; + } + + var episodeGrabbedEvent = new MovieGrabbedEvent(remoteMovie); + episodeGrabbedEvent.DownloadClient = downloadClient.GetType().Name; + + if (!string.IsNullOrWhiteSpace(downloadClientId)) + { + episodeGrabbedEvent.DownloadId = downloadClientId; + } + + _logger.ProgressInfo("Report sent to {0}. {1}", downloadClient.Definition.Name, downloadTitle); + _eventAggregator.PublishEvent(episodeGrabbedEvent); + } } } \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/IDownloadClient.cs b/src/NzbDrone.Core/Download/IDownloadClient.cs index 6703d8a22..ecf76844e 100644 --- a/src/NzbDrone.Core/Download/IDownloadClient.cs +++ b/src/NzbDrone.Core/Download/IDownloadClient.cs @@ -10,6 +10,7 @@ public interface IDownloadClient : IProvider DownloadProtocol Protocol { get; } string Download(RemoteEpisode remoteEpisode); + string Download(RemoteMovie remoteMovie); IEnumerable GetItems(); void RemoveItem(string downloadId, bool deleteData); DownloadClientStatus GetStatus(); diff --git a/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs new file mode 100644 index 000000000..cb331b24a --- /dev/null +++ b/src/NzbDrone.Core/Download/MovieGrabbedEvent.cs @@ -0,0 +1,17 @@ +using NzbDrone.Common.Messaging; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download +{ + public class MovieGrabbedEvent : IEvent + { + public RemoteMovie Movie { get; private set; } + public string DownloadClient { get; set; } + public string DownloadId { get; set; } + + public MovieGrabbedEvent(RemoteMovie movie) + { + Movie = movie; + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Core/Download/TorrentClientBase.cs b/src/NzbDrone.Core/Download/TorrentClientBase.cs index b1fcd7e2e..83c013ad7 100644 --- a/src/NzbDrone.Core/Download/TorrentClientBase.cs +++ b/src/NzbDrone.Core/Download/TorrentClientBase.cs @@ -40,35 +40,43 @@ protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader, protected abstract string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink); protected abstract string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent); - - public override string Download(RemoteEpisode remoteEpisode) + protected virtual string AddFromMagnetLink(RemoteMovie remoteMovie, string hash, string magnetLink) { - var torrentInfo = remoteEpisode.Release as TorrentInfo; + throw new NotImplementedException(); + } + protected virtual string AddFromTorrentFile(RemoteMovie remoteMovie, string hash, string filename, byte[] fileContent) + { + throw new NotImplementedException(); + } + + public override string Download(RemoteMovie remoteMovie) + { + var torrentInfo = remoteMovie.Release as TorrentInfo; string magnetUrl = null; string torrentUrl = null; - if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteEpisode.Release.DownloadUrl.StartsWith("magnet:")) + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) { - magnetUrl = remoteEpisode.Release.DownloadUrl; + magnetUrl = remoteMovie.Release.DownloadUrl; } else { - torrentUrl = remoteEpisode.Release.DownloadUrl; + torrentUrl = remoteMovie.Release.DownloadUrl; } if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) { magnetUrl = torrentInfo.MagnetUrl; } - + if (PreferTorrentFile) { if (torrentUrl.IsNotNullOrWhiteSpace()) { try { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } catch (Exception ex) { @@ -85,11 +93,11 @@ public override string Download(RemoteEpisode remoteEpisode) { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } } } @@ -99,13 +107,13 @@ public override string Download(RemoteEpisode remoteEpisode) { try { - return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); } catch (NotSupportedException ex) { if (torrentUrl.IsNullOrWhiteSpace()) { - throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); } _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); @@ -114,13 +122,193 @@ public override string Download(RemoteEpisode remoteEpisode) if (torrentUrl.IsNotNullOrWhiteSpace()) { - return DownloadFromWebUrl(remoteEpisode, torrentUrl); + return DownloadFromWebUrl(remoteMovie, torrentUrl); } } return null; } + public override string Download(RemoteEpisode remoteMovie) + { + var torrentInfo = remoteMovie.Release as TorrentInfo; + + string magnetUrl = null; + string torrentUrl = null; + + if (remoteMovie.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteMovie.Release.DownloadUrl.StartsWith("magnet:")) + { + magnetUrl = remoteMovie.Release.DownloadUrl; + } + else + { + torrentUrl = remoteMovie.Release.DownloadUrl; + } + + if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) + { + magnetUrl = torrentInfo.MagnetUrl; + } + + if (PreferTorrentFile) + { + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + catch (Exception ex) + { + if (!magnetUrl.IsNullOrWhiteSpace()) + { + throw; + } + + _logger.Debug("Torrent download failed, trying magnet. ({0})", ex.Message); + } + } + + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + } + } + else + { + if (magnetUrl.IsNotNullOrWhiteSpace()) + { + try + { + return DownloadFromMagnetUrl(remoteMovie, magnetUrl); + } + catch (NotSupportedException ex) + { + if (torrentUrl.IsNullOrWhiteSpace()) + { + throw new ReleaseDownloadException(remoteMovie.Release, "Magnet not supported by download client. ({0})", ex.Message); + } + + _logger.Debug("Magnet not supported by download client, trying torrent. ({0})", ex.Message); + } + } + + if (torrentUrl.IsNotNullOrWhiteSpace()) + { + return DownloadFromWebUrl(remoteMovie, torrentUrl); + } + } + + return null; + } + + private string DownloadFromWebUrl(RemoteMovie remoteEpisode, string torrentUrl) + { + byte[] torrentFile = null; + + try + { + var request = new HttpRequest(torrentUrl); + request.Headers.Accept = "application/x-bittorrent"; + request.AllowAutoRedirect = false; + + var response = _httpClient.Get(request); + + if (response.StatusCode == HttpStatusCode.SeeOther || response.StatusCode == HttpStatusCode.Found) + { + var locationHeader = response.Headers.GetSingleValue("Location"); + + _logger.Trace("Torrent request is being redirected to: {0}", locationHeader); + + if (locationHeader != null) + { + if (locationHeader.StartsWith("magnet:")) + { + return DownloadFromMagnetUrl(remoteEpisode, locationHeader); + } + + return DownloadFromWebUrl(remoteEpisode, locationHeader); + } + + throw new WebException("Remote website tried to redirect without providing a location."); + } + + torrentFile = response.ResponseData; + + _logger.Debug("Downloading torrent for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, torrentFile.Length, torrentUrl); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", torrentUrl); + } + else + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading torrent file for episode '{0}' failed ({1})", remoteEpisode.Release.Title, torrentUrl); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading torrent failed", ex); + } + + var filename = string.Format("{0}.torrent", FileNameBuilder.CleanFileName(remoteEpisode.Release.Title)); + var hash = _torrentFileInfoReader.GetHashFromTorrentFile(torrentFile); + var actualHash = AddFromTorrentFile(remoteEpisode, hash, filename, torrentFile); + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + + private string DownloadFromMagnetUrl(RemoteMovie remoteEpisode, string magnetUrl) + { + string hash = null; + string actualHash = null; + + try + { + hash = new MagnetLink(magnetUrl).InfoHash.ToHex(); + } + catch (FormatException ex) + { + _logger.Error(ex, "Failed to parse magnetlink for episode '{0}': '{1}'", remoteEpisode.Release.Title, magnetUrl); + + return null; + } + + if (hash != null) + { + actualHash = AddFromMagnetLink(remoteEpisode, hash, magnetUrl); + } + + if (actualHash.IsNotNullOrWhiteSpace() && hash != actualHash) + { + _logger.Debug( + "{0} did not return the expected InfoHash for '{1}', Sonarr could potentially lose track of the download in progress.", + Definition.Implementation, remoteEpisode.Release.DownloadUrl); + } + + return actualHash; + } + private string DownloadFromWebUrl(RemoteEpisode remoteEpisode, string torrentUrl) { byte[] torrentFile = null; diff --git a/src/NzbDrone.Core/Download/UsenetClientBase.cs b/src/NzbDrone.Core/Download/UsenetClientBase.cs index a6c0ed7d5..e45bb2d89 100644 --- a/src/NzbDrone.Core/Download/UsenetClientBase.cs +++ b/src/NzbDrone.Core/Download/UsenetClientBase.cs @@ -1,4 +1,5 @@ using System.Net; +using System; using NzbDrone.Common.Disk; using NzbDrone.Common.Http; using NzbDrone.Core.Exceptions; @@ -31,6 +32,11 @@ protected UsenetClientBase(IHttpClient httpClient, protected abstract string AddFromNzbFile(RemoteEpisode remoteEpisode, string filename, byte[] fileContent); + protected virtual string AddFromNzbFile(RemoteMovie remoteMovie, string filename, byte[] fileContents) + { + throw new NotImplementedException(); + } + public override string Download(RemoteEpisode remoteEpisode) { var url = remoteEpisode.Release.DownloadUrl; @@ -67,5 +73,42 @@ public override string Download(RemoteEpisode remoteEpisode) _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); return AddFromNzbFile(remoteEpisode, filename, nzbData); } + + public override string Download(RemoteMovie remoteEpisode) + { + var url = remoteEpisode.Release.DownloadUrl; + var filename = FileNameBuilder.CleanFileName(remoteEpisode.Release.Title) + ".nzb"; + + byte[] nzbData; + + try + { + nzbData = _httpClient.Get(new HttpRequest(url)).ResponseData; + + _logger.Debug("Downloaded nzb for episode '{0}' finished ({1} bytes from {2})", remoteEpisode.Release.Title, nzbData.Length, url); + } + catch (HttpException ex) + { + if ((int)ex.Response.StatusCode == 429) + { + _logger.Error("API Grab Limit reached for {0}", url); + } + else + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + } + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + catch (WebException ex) + { + _logger.Error(ex, "Downloading nzb for episode '{0}' failed ({1})", remoteEpisode.Release.Title, url); + + throw new ReleaseDownloadException(remoteEpisode.Release, "Downloading nzb failed", ex); + } + + _logger.Info("Adding report [{0}] to the queue.", remoteEpisode.Release.Title); + return AddFromNzbFile(remoteEpisode, filename, nzbData); + } } } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 01c680406..4bc218fe3 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -457,6 +457,7 @@ +