mirror of
https://github.com/Readarr/Readarr.git
synced 2026-04-28 23:06:43 -04:00
Added tests and refactored TorrentRss code.
This commit is contained in:
@@ -217,7 +217,7 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
if (releases.Empty())
|
||||
{
|
||||
return new ValidationFailure("Url", "No results were returned from your indexer, please check your settings.");
|
||||
return new ValidationFailure(string.Empty, "No results were returned from your indexer, please check your settings.");
|
||||
}
|
||||
}
|
||||
catch (ApiKeyException)
|
||||
@@ -230,11 +230,17 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
_logger.Warn("Request limit reached");
|
||||
}
|
||||
catch (UnsupportedFeedException ex)
|
||||
{
|
||||
_logger.WarnException("Indexer feed is not supported: " + ex.Message, ex);
|
||||
|
||||
return new ValidationFailure(string.Empty, "Indexer feed is not supported: " + ex.Message);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Unable to connect to indexer: " + ex.Message, ex);
|
||||
|
||||
return new ValidationFailure("Url", "Unable to connect to indexer, check the log for more details");
|
||||
return new ValidationFailure(string.Empty, "Unable to connect to indexer, check the log for more details");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -156,6 +156,11 @@ namespace NzbDrone.Core.Indexers
|
||||
{
|
||||
var dateString = item.TryGetValue("pubDate");
|
||||
|
||||
if (dateString.IsNullOrWhiteSpace())
|
||||
{
|
||||
throw new UnsupportedFeedException("Rss feed must have a pubDate element with a valid publish date.");
|
||||
}
|
||||
|
||||
return XElementExtensions.ParseDate(dateString);
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -4,10 +4,18 @@ using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Parser;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public class TorrentRssIndexer : HttpIndexerBase<TorrentRssIndexerSettings>
|
||||
{
|
||||
public override string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
return "Torrent RSS Feed";
|
||||
}
|
||||
}
|
||||
|
||||
public override DownloadProtocol Protocol { get { return DownloadProtocol.Torrent; } }
|
||||
public override Boolean SupportsSearch { get { return false; } }
|
||||
public override Int32 PageSize { get { return 0; } }
|
||||
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public class TorrentRssIndexerParserSettings
|
||||
{
|
||||
public bool UseEZTVFormat { get; set; }
|
||||
public bool ParseSeedersInDescription { get; set; }
|
||||
public bool UseEnclosureLength { get; set; }
|
||||
public bool ParseSizeInDescription { get; set; }
|
||||
public string SizeElementName { get; set; }
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -4,7 +4,7 @@ using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public class TorrentRssIndexerRequestGenerator : IIndexerRequestGenerator
|
||||
{
|
||||
@@ -46,7 +46,14 @@ namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
|
||||
private IEnumerable<IndexerRequest> GetRssRequests(String searchParameters)
|
||||
{
|
||||
yield return new IndexerRequest(Settings.BaseUrl.Trim().TrimEnd('/'), HttpAccept.Rss);
|
||||
var request = new IndexerRequest(Settings.BaseUrl.Trim().TrimEnd('/'), HttpAccept.Rss);
|
||||
|
||||
if (Settings.Cookie.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
request.HttpRequest.AddCookie(Settings.Cookie);
|
||||
}
|
||||
|
||||
yield return request;
|
||||
}
|
||||
}
|
||||
}
|
||||
+9
-2
@@ -4,7 +4,7 @@ using NzbDrone.Core.Annotations;
|
||||
using NzbDrone.Core.ThingiProvider;
|
||||
using NzbDrone.Core.Validation;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public class TorrentRssIndexerSettingsValidator : AbstractValidator<TorrentRssIndexerSettings>
|
||||
{
|
||||
@@ -21,10 +21,17 @@ namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
public TorrentRssIndexerSettings()
|
||||
{
|
||||
BaseUrl = string.Empty;
|
||||
AllowZeroSize = false;
|
||||
}
|
||||
|
||||
[FieldDefinition(0, Label = "Full RSS Feed URL")]
|
||||
public String BaseUrl { get; set; }
|
||||
public string BaseUrl { get; set; }
|
||||
|
||||
[FieldDefinition(1, Label = "Cookie", HelpText = "If you site requires a login cookie to access the rss, you'll have to retrieve it via a browser.")]
|
||||
public string Cookie { get; set; }
|
||||
|
||||
[FieldDefinition(2, Type = FieldType.Checkbox, Label = "Allow Zero Size", HelpText="Enabling this will allow you to use feeds that don't specify release size, but be careful, size related checks will not be performed.")]
|
||||
public bool AllowZeroSize { get; set; }
|
||||
|
||||
public NzbDroneValidationResult Validate()
|
||||
{
|
||||
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public interface ITorrentRssParserFactory
|
||||
{
|
||||
TorrentRssParser GetParser(TorrentRssIndexerSettings settings);
|
||||
}
|
||||
|
||||
public class TorrentRssParserFactory : ITorrentRssParserFactory
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
private readonly ICached<TorrentRssIndexerParserSettings> _settingsCache;
|
||||
|
||||
private readonly ITorrentRssSettingsDetector _torrentRssSettingsDetector;
|
||||
|
||||
public TorrentRssParserFactory(ICacheManager cacheManager, ITorrentRssSettingsDetector torrentRssSettingsDetector, Logger logger)
|
||||
{
|
||||
_settingsCache = cacheManager.GetCache<TorrentRssIndexerParserSettings>(GetType());
|
||||
_torrentRssSettingsDetector = torrentRssSettingsDetector;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public TorrentRssParser GetParser(TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
var key = indexerSettings.ToJson();
|
||||
var parserSettings = _settingsCache.Get(key, () => DetectParserSettings(indexerSettings), TimeSpan.FromDays(7));
|
||||
|
||||
if (parserSettings.UseEZTVFormat)
|
||||
{
|
||||
return new EzrssTorrentRssParser();
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TorrentRssParser
|
||||
{
|
||||
UseGuidInfoUrl = false,
|
||||
ParseSeedersInDescription = parserSettings.ParseSeedersInDescription,
|
||||
|
||||
UseEnclosureLength = parserSettings.UseEnclosureLength,
|
||||
ParseSizeInDescription = parserSettings.ParseSizeInDescription,
|
||||
SizeElementName = parserSettings.SizeElementName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private TorrentRssIndexerParserSettings DetectParserSettings(TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
var settings = _torrentRssSettingsDetector.Detect(indexerSettings);
|
||||
|
||||
if (settings == null)
|
||||
{
|
||||
throw new UnsupportedFeedException("Could not parse feed from {0}", indexerSettings.BaseUrl);
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRss
|
||||
{
|
||||
public interface ITorrentRssSettingsDetector
|
||||
{
|
||||
TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings);
|
||||
}
|
||||
|
||||
public class TorrentRssSettingsDetector : ITorrentRssSettingsDetector
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
private const long ValidSizeThreshold = 2 * 1024 * 1024;
|
||||
|
||||
public TorrentRssSettingsDetector(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect settings for Parser, based on URL
|
||||
/// </summary>
|
||||
/// <param name="settings">Indexer Settings to use for Parser</param>
|
||||
/// <returns>Parsed Settings or <c>null</c></returns>
|
||||
public TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
_logger.Debug("Evaluating TorrentRss feed '{0}'", indexerSettings.BaseUrl);
|
||||
|
||||
var requestGenerator = new TorrentRssIndexerRequestGenerator { Settings = indexerSettings };
|
||||
var request = requestGenerator.GetRecentRequests().First().First();
|
||||
|
||||
HttpResponse httpResponse = null;
|
||||
try
|
||||
{
|
||||
httpResponse = _httpClient.Execute(request.HttpRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException(string.Format("Unable to connect to indexer {0}: {1}", request.Url, ex.Message), ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexerResponse = new IndexerResponse(request, httpResponse);
|
||||
return GetParserSettings(indexerResponse, indexerSettings);
|
||||
}
|
||||
|
||||
private TorrentRssIndexerParserSettings GetParserSettings(IndexerResponse response, TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
var settings = GetEzrssParserSettings(response, indexerSettings);
|
||||
if (settings != null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
settings = GetGenericTorrentRssParserSettings(response, indexerSettings);
|
||||
if (settings != null)
|
||||
{
|
||||
return settings;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private TorrentRssIndexerParserSettings GetEzrssParserSettings(IndexerResponse response, TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
if (!IsEZTVFeed(response))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.Trace("Feed has Ezrss schema");
|
||||
|
||||
var parser = new EzrssTorrentRssParser();
|
||||
var releases = ParseResponse(parser, response);
|
||||
|
||||
try
|
||||
{
|
||||
ValidateReleases(releases, indexerSettings);
|
||||
ValidateReleaseSize(releases, indexerSettings);
|
||||
|
||||
_logger.Debug("Feed was parseable by Ezrss Parser");
|
||||
return new TorrentRssIndexerParserSettings
|
||||
{
|
||||
UseEZTVFormat = true
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.TraceException("Feed wasn't parsable by Ezrss Parser", ex);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private TorrentRssIndexerParserSettings GetGenericTorrentRssParserSettings(IndexerResponse response, TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
var settings = new TorrentRssIndexerParserSettings
|
||||
{
|
||||
UseEnclosureLength = true,
|
||||
ParseSeedersInDescription = true
|
||||
};
|
||||
|
||||
var parser = new TorrentRssParser
|
||||
{
|
||||
UseEnclosureLength = true,
|
||||
ParseSeedersInDescription = true
|
||||
};
|
||||
|
||||
var releases = ParseResponse(parser, response);
|
||||
ValidateReleases(releases, indexerSettings);
|
||||
|
||||
if (!releases.Any(v => v.Seeders.HasValue))
|
||||
{
|
||||
_logger.Trace("Feed doesn't have Seeders in Description, disabling option.");
|
||||
parser.ParseSeedersInDescription = settings.ParseSeedersInDescription = false;
|
||||
}
|
||||
|
||||
if (!releases.Any(r => r.Size < ValidSizeThreshold))
|
||||
{
|
||||
_logger.Trace("Feed has valid size in enclosure.");
|
||||
return settings;
|
||||
}
|
||||
|
||||
parser.UseEnclosureLength = settings.UseEnclosureLength = false;
|
||||
parser.ParseSizeInDescription = settings.ParseSizeInDescription = true;
|
||||
|
||||
releases = ParseResponse(parser, response);
|
||||
ValidateReleases(releases, indexerSettings);
|
||||
|
||||
if (!releases.Any(r => r.Size < ValidSizeThreshold))
|
||||
{
|
||||
_logger.Trace("Feed has valid size in description.");
|
||||
return settings;
|
||||
}
|
||||
|
||||
parser.ParseSizeInDescription = settings.ParseSizeInDescription = false;
|
||||
parser.SizeElementName = settings.SizeElementName = "Size";
|
||||
|
||||
releases = ParseResponse(parser, response);
|
||||
ValidateReleases(releases, indexerSettings);
|
||||
|
||||
if (!releases.Any(r => r.Size < ValidSizeThreshold))
|
||||
{
|
||||
_logger.Trace("Feed has valid size in Size element.");
|
||||
return settings;
|
||||
}
|
||||
|
||||
_logger.Debug("Feed doesn't have release size.");
|
||||
|
||||
parser.SizeElementName = settings.SizeElementName = null;
|
||||
|
||||
releases = ParseResponse(parser, response);
|
||||
ValidateReleases(releases, indexerSettings);
|
||||
ValidateReleaseSize(releases, indexerSettings);
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
private Boolean IsEZTVFeed(IndexerResponse response)
|
||||
{
|
||||
using (var xmlTextReader = XmlReader.Create(new StringReader(response.Content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse, ValidationType = ValidationType.None, IgnoreComments = true, XmlResolver = null }))
|
||||
{
|
||||
var document = XDocument.Load(xmlTextReader);
|
||||
|
||||
// Check Namespace
|
||||
if (document.Root == null)
|
||||
{
|
||||
throw new InvalidDataException("Could not parse IndexerResponse into XML.");
|
||||
}
|
||||
|
||||
var ns = document.Root.GetNamespaceOfPrefix("torrent");
|
||||
if (ns == "http://xmlns.ezrss.it/0.1/")
|
||||
{
|
||||
_logger.Trace("Identified feed as EZTV compatible by EZTV Namespace");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check DTD in DocType
|
||||
if (document.DocumentType != null && document.DocumentType.SystemId == "http://xmlns.ezrss.it/0.1/dtd/")
|
||||
{
|
||||
_logger.Trace("Identified feed as EZTV compatible by EZTV DTD");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private TorrentInfo[] ParseResponse(IParseIndexerResponse parser, IndexerResponse response)
|
||||
{
|
||||
try
|
||||
{
|
||||
var releases = parser.ParseResponse(response).Cast<TorrentInfo>().ToArray();
|
||||
return releases;
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.DebugException("Unable to parse indexer feed: " + ex.Message, ex);
|
||||
throw new UnsupportedFeedException("Unable to parse indexer: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateReleases(TorrentInfo[] releases, TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
if (releases == null || releases.Empty())
|
||||
{
|
||||
throw new UnsupportedFeedException("Empty feed, cannot check if feed is parsable.");
|
||||
}
|
||||
|
||||
var torrentInfo = releases.First();
|
||||
|
||||
_logger.Trace("TorrentInfo: \n{0}", torrentInfo.ToString("L"));
|
||||
|
||||
if (releases.Any(r => r.Title.IsNullOrWhiteSpace()))
|
||||
{
|
||||
throw new UnsupportedFeedException("Feed contains releases without title.");
|
||||
}
|
||||
|
||||
if (releases.Any(r => !IsValidDownloadUrl(r.DownloadUrl)))
|
||||
{
|
||||
throw new UnsupportedFeedException("Failed to find a valid download url in the feed.");
|
||||
}
|
||||
|
||||
var total = releases.Where(v => v.Guid != null).Select(v => v.Guid).ToArray();
|
||||
var distinct = total.Distinct().ToArray();
|
||||
|
||||
if (distinct.Length != total.Length)
|
||||
{
|
||||
|
||||
throw new UnsupportedFeedException("Feed contains releases with same guid, rejecting malformed rss feed.");
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateReleaseSize(TorrentInfo[] releases, TorrentRssIndexerSettings indexerSettings)
|
||||
{
|
||||
if (!indexerSettings.AllowZeroSize && releases.Any(r => r.Size == 0))
|
||||
{
|
||||
throw new UnsupportedFeedException("Feed doesn't contain the release content size.");
|
||||
}
|
||||
|
||||
if (releases.Any(r => r.Size != 0 && r.Size < ValidSizeThreshold))
|
||||
{
|
||||
throw new UnsupportedFeedException("Size of one more releases lower than {0}, feed must contain release content size.", ValidSizeThreshold.SizeSuffix());
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidDownloadUrl(string url)
|
||||
{
|
||||
if (url.IsNullOrWhiteSpace())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (url.StartsWith("magnet:") ||
|
||||
url.StartsWith("http:") ||
|
||||
url.StartsWith("https:"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
{
|
||||
public interface ITorrentRssParserFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Get configured Parser based on <paramref name="settings"/>
|
||||
/// </summary>
|
||||
/// <param name="settings">Indexer Settings to use for Parser</param>
|
||||
/// <returns>Configured Parser</returns>
|
||||
TorrentRssParser GetParser(TorrentRssIndexerSettings settings);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
{
|
||||
public interface ITorrentRssSettingsDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Detect settings for Parser, based on URL
|
||||
/// </summary>
|
||||
/// <param name="settings">Indexer Settings to use for Parser</param>
|
||||
/// <returns>Parsed Settings or <c>null</c></returns>
|
||||
TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
{
|
||||
public class TorrentRssIndexerParserSettings
|
||||
{
|
||||
public Boolean UseEZTVFormat { get; set; }
|
||||
|
||||
public Boolean ParseSeedersInDescription { get; set; }
|
||||
|
||||
public Boolean ParseSizeInDescription { get; set; }
|
||||
|
||||
public String SizeElementName { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
using System;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
{
|
||||
public class TorrentRssParserFactory : ITorrentRssParserFactory
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
private readonly ICached<TorrentRssIndexerParserSettings> _settingsCache;
|
||||
|
||||
private readonly ITorrentRssSettingsDetector _torrentRssSettingsDetector;
|
||||
|
||||
public TorrentRssParserFactory(ICacheManager cacheManager, ITorrentRssSettingsDetector torrentRssSettingsDetector, Logger logger)
|
||||
{
|
||||
_settingsCache = cacheManager.GetCache<TorrentRssIndexerParserSettings>(GetType());
|
||||
_torrentRssSettingsDetector = torrentRssSettingsDetector;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get configured Parser based on <paramref name="settings"/>
|
||||
/// </summary>
|
||||
/// <param name="settings">Indexer Settings to use for Parser</param>
|
||||
/// <returns>Configured Parser</returns>
|
||||
public TorrentRssParser GetParser(TorrentRssIndexerSettings settings)
|
||||
{
|
||||
TorrentRssIndexerParserSettings parserSettings = _settingsCache.Get(settings.BaseUrl,
|
||||
() =>
|
||||
{
|
||||
_logger.Debug("Parser Settings not in cache. Trying to parse feed {0}", settings.BaseUrl);
|
||||
var parserSettingsToStore = _torrentRssSettingsDetector.Detect(settings);
|
||||
|
||||
if (parserSettingsToStore == null)
|
||||
{
|
||||
throw new Exception(string.Format("Could not parse feed from {0}", settings.BaseUrl));
|
||||
}
|
||||
|
||||
return parserSettingsToStore;
|
||||
},
|
||||
new TimeSpan(7, 0, 0, 0));
|
||||
|
||||
if (parserSettings.UseEZTVFormat)
|
||||
{
|
||||
return new EzrssTorrentRssParser();
|
||||
}
|
||||
else
|
||||
{
|
||||
return new TorrentRssParser { UseGuidInfoUrl = false, ParseSeedersInDescription = parserSettings.ParseSeedersInDescription, ParseSizeInDescription = parserSettings.ParseSizeInDescription, SizeElementName = parserSettings.SizeElementName };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using System.Xml.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Http;
|
||||
using NzbDrone.Core.Indexers.Exceptions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.TorrentRssIndexer
|
||||
{
|
||||
public class TorrentRssSettingsDetector : ITorrentRssSettingsDetector
|
||||
{
|
||||
protected readonly Logger _logger;
|
||||
|
||||
protected TorrentRssIndexerSettings _settings;
|
||||
|
||||
private readonly IHttpClient _httpClient;
|
||||
|
||||
private const int ValidEntryPercentage = 100;
|
||||
|
||||
private const int ValidSizeThresholdMegabytes = 2;
|
||||
|
||||
public TorrentRssSettingsDetector(IHttpClient httpClient, Logger logger)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detect settings for Parser, based on URL
|
||||
/// </summary>
|
||||
/// <param name="settings">Indexer Settings to use for Parser</param>
|
||||
/// <returns>Parsed Settings or <c>null</c></returns>
|
||||
public TorrentRssIndexerParserSettings Detect(TorrentRssIndexerSettings settings)
|
||||
{
|
||||
_settings = settings;
|
||||
|
||||
var requestGenerator = new TorrentRssIndexerRequestGenerator { Settings = _settings };
|
||||
var request = requestGenerator.GetRecentRequests().First().First();
|
||||
if (request == null)
|
||||
{
|
||||
throw new NullReferenceException("request cannot be null.");
|
||||
}
|
||||
|
||||
HttpResponse httpResponse = null;
|
||||
try
|
||||
{
|
||||
httpResponse = _httpClient.Execute(request.HttpRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException(string.Format("Unable to connect to indexer {0}: {1}", request.Url, ex.Message), ex);
|
||||
return null;
|
||||
}
|
||||
|
||||
var indexerResponse = new IndexerResponse(request, httpResponse);
|
||||
return GetParserSettings(indexerResponse);
|
||||
}
|
||||
|
||||
private TorrentRssIndexerParserSettings GetParserSettings(IndexerResponse response)
|
||||
{
|
||||
var settings = new TorrentRssIndexerParserSettings();
|
||||
|
||||
var isEZTVFeed = IsEZTVFeed(response);
|
||||
_logger.Debug("Feed is EZTV Compatible: {0}", isEZTVFeed);
|
||||
|
||||
TorrentRssParser parser;
|
||||
if (isEZTVFeed)
|
||||
{
|
||||
// Test EZTV
|
||||
parser = new EzrssTorrentRssParser();
|
||||
var eztvTestResult = TestTorrentParser(response, parser);
|
||||
_logger.Debug("EZTV Parse result: {0}", eztvTestResult);
|
||||
|
||||
if (eztvTestResult)
|
||||
{
|
||||
_logger.Debug("Feed is a parseable EZTV Feed");
|
||||
settings.UseEZTVFormat = true;
|
||||
return settings;
|
||||
}
|
||||
}
|
||||
|
||||
parser = new TorrentRssParser
|
||||
{
|
||||
ParseSeedersInDescription = true,
|
||||
ParseSizeInDescription = true
|
||||
};
|
||||
|
||||
if (TestTorrentParser(response, parser))
|
||||
{
|
||||
_logger.Debug("Feed is a normal RSS Feed with Seeders and Size in Description");
|
||||
settings.UseEZTVFormat = false;
|
||||
settings.ParseSeedersInDescription = true;
|
||||
settings.ParseSizeInDescription = true;
|
||||
return settings;
|
||||
}
|
||||
|
||||
parser = new TorrentRssParser
|
||||
{
|
||||
ParseSeedersInDescription = true,
|
||||
SizeElementName = "Size"
|
||||
};
|
||||
|
||||
if (TestTorrentParser(response, parser))
|
||||
{
|
||||
_logger.Debug("Feed is an RSS Feed with Seeders in Description and Size field is \"Size\"");
|
||||
settings.UseEZTVFormat = false;
|
||||
settings.ParseSeedersInDescription = true;
|
||||
settings.SizeElementName = "Size";
|
||||
return settings;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private Boolean IsEZTVFeed(IndexerResponse response)
|
||||
{
|
||||
using (var xmlTextReader = XmlReader.Create(new StringReader(response.Content), new XmlReaderSettings { DtdProcessing = DtdProcessing.Parse, ValidationType = ValidationType.None, IgnoreComments = true, XmlResolver = null }))
|
||||
{
|
||||
var document = XDocument.Load(xmlTextReader);
|
||||
|
||||
// Check Namespace
|
||||
if (document.Root == null)
|
||||
{
|
||||
throw new InvalidDataException("Could not parse IndexerResponse into XML.");
|
||||
}
|
||||
|
||||
var ns = document.Root.GetNamespaceOfPrefix("torrent");
|
||||
if (ns == "http://xmlns.ezrss.it/0.1/")
|
||||
{
|
||||
_logger.Trace("Identified feed as EZTV compatible by EZTV Namespace");
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check DTD in DocType
|
||||
if (document.DocumentType != null && document.DocumentType.SystemId == "http://xmlns.ezrss.it/0.1/dtd/")
|
||||
{
|
||||
_logger.Trace("Identified feed as EZTV compatible by EZTV DTD");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Boolean TestTorrentParser(IndexerResponse response, TorrentRssParser parser)
|
||||
{
|
||||
return ExecuteWithExceptionHandling(
|
||||
() =>
|
||||
{
|
||||
var releases = parser.ParseResponse(response).ToList();
|
||||
|
||||
if (releases.Empty())
|
||||
{
|
||||
_logger.Trace("Empty releases");
|
||||
return false;
|
||||
}
|
||||
|
||||
var firstRelease = releases.First();
|
||||
var torrentInfo = firstRelease as TorrentInfo;
|
||||
if (torrentInfo == null)
|
||||
{
|
||||
_logger.Trace("Not TorrentInfo");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (torrentInfo.Size == 0 || string.IsNullOrEmpty(torrentInfo.Title) || string.IsNullOrEmpty(torrentInfo.DownloadUrl))
|
||||
{
|
||||
_logger.Trace("Failed Parsing. Content: \n{0}", torrentInfo.ToString("L"));
|
||||
_logger.Trace(response.Content);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateTorrents(releases, r => Parser.Parser.ParseTitle(r.Title) != null, ValidEntryPercentage))
|
||||
{
|
||||
_logger.Trace("Percentage of Titles that could parsed is lower than threshold of {0}", ValidEntryPercentage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateTorrents(releases, r => r.Size > ValidSizeThresholdMegabytes * 1024 * 1024, ValidEntryPercentage))
|
||||
{
|
||||
_logger.Trace("Percentage of entries that have a size bigger than ValidSizeThresholdMegabytes ({0} MB) is lower than threshold of {1}", ValidSizeThresholdMegabytes, ValidEntryPercentage);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ValidateTorrents(releases, r => Uri.IsWellFormedUriString(r.DownloadUrl, UriKind.Absolute), ValidEntryPercentage))
|
||||
{
|
||||
_logger.Trace("Percentage of entries that have a valid download url is smaller than threshold of {0}", ValidEntryPercentage);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private Boolean ValidateTorrents(IEnumerable<ReleaseInfo> releases, Func<ReleaseInfo, Boolean> entryValidationFunction, int threshold)
|
||||
{
|
||||
var validEntries = releases.Count(entryValidationFunction);
|
||||
var validEntriesPercentage = validEntries * 1.0 / releases.Count() * 100.0;
|
||||
return validEntriesPercentage >= threshold;
|
||||
}
|
||||
|
||||
private Boolean ExecuteWithExceptionHandling(Func<Boolean> functionToExecute)
|
||||
{
|
||||
try
|
||||
{
|
||||
return functionToExecute();
|
||||
}
|
||||
catch (ApiKeyException)
|
||||
{
|
||||
_logger.Warn("Indexer returned result for RSS URL, API Key appears to be invalid");
|
||||
|
||||
return false;
|
||||
}
|
||||
catch (RequestLimitReachedException)
|
||||
{
|
||||
_logger.Warn("Request limit reached");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.WarnException("Unable to connect to indexer: " + ex.Message, ex);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,30 @@ namespace NzbDrone.Core.Indexers
|
||||
|
||||
protected virtual String GetInfoHash(XElement item)
|
||||
{
|
||||
var magnetUrl = GetMagnetUrl(item);
|
||||
if (magnetUrl.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
try
|
||||
{
|
||||
var magnetLink = new MonoTorrent.MagnetLink(magnetUrl);
|
||||
return magnetLink.InfoHash.ToHex();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected virtual String GetMagnetUrl(XElement item)
|
||||
{
|
||||
var downloadUrl = GetDownloadUrl(item);
|
||||
if (downloadUrl.StartsWith("magnet:"))
|
||||
{
|
||||
return downloadUrl;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -41,10 +41,10 @@ namespace NzbDrone.Core.Indexers
|
||||
try
|
||||
{
|
||||
DateTime result;
|
||||
if (!DateTime.TryParse(dateString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal, out result))
|
||||
if (!DateTime.TryParse(dateString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal, out result))
|
||||
{
|
||||
dateString = RemoveTimeZoneRegex.Replace(dateString, "");
|
||||
result = DateTime.Parse(dateString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AdjustToUniversal);
|
||||
result = DateTime.Parse(dateString, DateTimeFormatInfo.InvariantInfo, DateTimeStyles.AssumeUniversal);
|
||||
}
|
||||
return result.ToUniversalTime();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user