using System; using System.Collections.Generic; using System.Linq; using FluentValidation; using FluentValidation.Results; using NLog; using NzbDrone.Common; using NzbDrone.Common.Http; using NzbDrone.Core.Indexers.Exceptions; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers.Newznab { public class Newznab : IndexerBase { private readonly IFetchFeedFromIndexers _feedFetcher; private readonly HttpProvider _httpProvider; private readonly Logger _logger; public Newznab(IFetchFeedFromIndexers feedFetcher, HttpProvider httpProvider, Logger logger) { _feedFetcher = feedFetcher; _httpProvider = httpProvider; _logger = logger; } //protected so it can be mocked, but not used for DI //TODO: Is there a better way to achieve this? protected Newznab() { } public override DownloadProtocol Protocol { get { return DownloadProtocol.Usenet; } } public override Int32 SupportedPageSize { get { return 100; } } public override IParseFeed Parser { get { return new NewznabParser(); } } public override IEnumerable DefaultDefinitions { get { var list = new List(); list.Add(new IndexerDefinition { Enable = false, Name = "Nzbs.org", Implementation = GetType().Name, Settings = GetSettings("http://nzbs.org", new List { 5000 }) }); list.Add(new IndexerDefinition { Enable = false, Name = "Nzb.su", Implementation = GetType().Name, Settings = GetSettings("https://api.nzb.su", new List()) }); list.Add(new IndexerDefinition { Enable = false, Name = "Dognzb.cr", Implementation = GetType().Name, Settings = GetSettings("https://api.dognzb.cr", new List()) }); list.Add(new IndexerDefinition { Enable = false, Name = "OZnzb.com", Implementation = GetType().Name, Settings = GetSettings("https://www.oznzb.com", new List()) }); list.Add(new IndexerDefinition { Enable = false, Name = "nzbplanet.net", Implementation = GetType().Name, Settings = GetSettings("https://nzbplanet.net", new List()) }); list.Add(new IndexerDefinition { Enable = false, Name = "NZBgeek", Implementation = GetType().Name, Settings = GetSettings("https://api.nzbgeek.info", new List()) }); return list; } } public override ProviderDefinition Definition { get; set; } private NewznabSettings GetSettings(string url, List categories) { var settings = new NewznabSettings { Url = url }; if (categories.Any()) { settings.Categories = categories; } return settings; } public override IEnumerable RecentFeed { get { var categories = String.Join(",", Settings.Categories.Concat(Settings.AnimeCategories)); var url = String.Format("{0}/api?t=tvsearch&cat={1}&extended=1{2}", Settings.Url.TrimEnd('/'), categories, Settings.AdditionalParameters); if (!String.IsNullOrWhiteSpace(Settings.ApiKey)) { url += "&apikey=" + Settings.ApiKey; } yield return url; } } public override IEnumerable GetEpisodeSearchUrls(List titles, int tvRageId, int seasonNumber, int episodeNumber) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&ep={3}", url, tvRageId, seasonNumber, episodeNumber)); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(title), seasonNumber, episodeNumber))); } public override IEnumerable GetDailyEpisodeSearchUrls(List titles, int tvRageId, DateTime date) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, tvRageId, date)).ToList(); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2:yyyy}&ep={2:MM}/{2:dd}", url, NewsnabifyTitle(title), date)).ToList()); } public override IEnumerable GetAnimeEpisodeSearchUrls(List titles, int tvRageId, int absoluteEpisodeNumber) { if (Settings.AnimeCategories.Empty()) { return Enumerable.Empty(); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}+{2:00}", url.Replace("t=tvsearch", "t=search"), NewsnabifyTitle(title), absoluteEpisodeNumber))); } public override IEnumerable GetSeasonSearchUrls(List titles, int tvRageId, int seasonNumber, int offset) { if (Settings.Categories.Empty()) { return Enumerable.Empty(); } if (tvRageId > 0) { return RecentFeed.Select(url => String.Format("{0}&limit=100&rid={1}&season={2}&offset={3}", url, tvRageId, seasonNumber, offset)); } return titles.SelectMany(title => RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&offset={3}", url, NewsnabifyTitle(title), seasonNumber, offset))); } public override IEnumerable GetSearchUrls(string query, int offset) { // encode query (replace the + with spaces first) query = query.Replace("+", " "); query = System.Web.HttpUtility.UrlEncode(query); return RecentFeed.Select(url => String.Format("{0}&offset={1}&limit=100&q={2}", url.Replace("t=tvsearch", "t=search"), offset, query)); } public override ValidationResult Test() { var releases = _feedFetcher.FetchRss(this); if (releases.Any()) return new ValidationResult(); try { var url = RecentFeed.First(); var xml = _httpProvider.DownloadString(url); NewznabPreProcessor.Process(xml, url); } catch (ApiKeyException) { _logger.Warn("Indexer returned result for Newznab RSS URL, API Key appears to be invalid"); var apiKeyFailure = new ValidationFailure("ApiKey", "Invalid API Key"); return new ValidationResult(new List { apiKeyFailure }); } catch (RequestLimitReachedException) { _logger.Warn("Request limit reached"); } catch (Exception ex) { _logger.WarnException("Unable to connect to indexer: " + ex.Message, ex); var failure = new ValidationFailure("Url", "Unable to connect to indexer, check the log for more details"); return new ValidationResult(new List { failure }); } return new ValidationResult(); } private static string NewsnabifyTitle(string title) { return title.Replace("+", "%20"); } } }