using System; using System.Net; using System.Threading.Tasks; using MonoTorrent; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.Extensions; using NzbDrone.Common.Http; using NzbDrone.Core.Configuration; using NzbDrone.Core.Exceptions; using NzbDrone.Core.Indexers; using NzbDrone.Core.Localization; using NzbDrone.Core.MediaFiles.TorrentInfo; using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.RemotePathMappings; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Download { public abstract class TorrentClientBase : DownloadClientBase where TSettings : IProviderConfig, new() { protected readonly IHttpClient _httpClient; protected readonly ITorrentFileInfoReader _torrentFileInfoReader; protected TorrentClientBase(ITorrentFileInfoReader torrentFileInfoReader, IHttpClient httpClient, IConfigService configService, IDiskProvider diskProvider, IRemotePathMappingService remotePathMappingService, Logger logger, ILocalizationService localizationService) : base(configService, diskProvider, remotePathMappingService, logger, localizationService) { _httpClient = httpClient; _torrentFileInfoReader = torrentFileInfoReader; } public override DownloadProtocol Protocol => DownloadProtocol.Torrent; public virtual bool PreferTorrentFile => false; protected abstract string AddFromMagnetLink(RemoteEpisode remoteEpisode, string hash, string magnetLink); protected abstract string AddFromTorrentFile(RemoteEpisode remoteEpisode, string hash, string filename, byte[] fileContent); public override async Task Download(RemoteEpisode remoteEpisode, IIndexer indexer) { var torrentInfo = remoteEpisode.Release as TorrentInfo; string magnetUrl = null; string torrentUrl = null; if (remoteEpisode.Release.DownloadUrl.IsNotNullOrWhiteSpace() && remoteEpisode.Release.DownloadUrl.StartsWith("magnet:")) { magnetUrl = remoteEpisode.Release.DownloadUrl; } else { torrentUrl = remoteEpisode.Release.DownloadUrl; } if (torrentInfo != null && !torrentInfo.MagnetUrl.IsNullOrWhiteSpace()) { magnetUrl = torrentInfo.MagnetUrl; } if (PreferTorrentFile) { if (torrentUrl.IsNotNullOrWhiteSpace()) { try { return await DownloadFromWebUrl(remoteEpisode, indexer, torrentUrl); } catch (Exception ex) { if (!magnetUrl.IsNullOrWhiteSpace()) { throw; } _logger.Debug("Torrent download failed, trying magnet. ({0})", ex.Message); } } if (magnetUrl.IsNotNullOrWhiteSpace()) { try { return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); } catch (NotSupportedException ex) { throw new ReleaseDownloadException(remoteEpisode.Release, "Magnet not supported by download client. ({0})", ex.Message); } } } else { if (magnetUrl.IsNotNullOrWhiteSpace()) { try { return DownloadFromMagnetUrl(remoteEpisode, magnetUrl); } catch (NotSupportedException ex) { if (torrentUrl.IsNullOrWhiteSpace()) { throw new ReleaseDownloadException(remoteEpisode.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 await DownloadFromWebUrl(remoteEpisode, indexer, torrentUrl); } } return null; } private async Task DownloadFromWebUrl(RemoteEpisode remoteEpisode, IIndexer indexer, string torrentUrl) { byte[] torrentFile = null; try { var request = indexer?.GetDownloadRequest(torrentUrl) ?? new HttpRequest(torrentUrl); request.RateLimitKey = remoteEpisode?.Release?.IndexerId.ToString(); request.Headers.Accept = "application/x-bittorrent"; request.AllowAutoRedirect = false; var response = await _httpClient.GetAsync(request); if (response.StatusCode == HttpStatusCode.MovedPermanently || response.StatusCode == HttpStatusCode.Found || response.StatusCode == HttpStatusCode.SeeOther) { 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); } request.Url += new HttpUri(locationHeader); return await DownloadFromWebUrl(remoteEpisode, indexer, request.Url.ToString()); } 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 (ex.Response.StatusCode == HttpStatusCode.NotFound) { _logger.Error(ex, "Downloading torrent file for episode '{0}' failed since it no longer exists ({1})", remoteEpisode.Release.Title, torrentUrl); throw new ReleaseUnavailableException(remoteEpisode.Release, "Downloading torrent failed", 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(RemoteEpisode remoteEpisode, string magnetUrl) { string hash = null; string actualHash = null; try { hash = MagnetLink.Parse(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; } } }