using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.ServiceModel.Syndication; using System.Text.RegularExpressions; using Ninject; using NLog; using NzbDrone.Common; using NzbDrone.Core.Model; using NzbDrone.Core.Providers.Core; namespace NzbDrone.Core.Providers.Indexer { public abstract class IndexerBase { protected readonly Logger _logger; private readonly HttpProvider _httpProvider; protected readonly ConfigProvider _configProvider; protected static readonly Regex TitleSearchRegex = new Regex(@"[\W]", RegexOptions.IgnoreCase | RegexOptions.Compiled); protected static readonly Regex RemoveThe = new Regex(@"^the\s", RegexOptions.IgnoreCase | RegexOptions.Compiled); [Inject] protected IndexerBase(HttpProvider httpProvider, ConfigProvider configProvider) { _httpProvider = httpProvider; _configProvider = configProvider; _logger = LogManager.GetLogger(GetType().ToString()); } public IndexerBase() { } /// <summary> /// Gets the name for the feed /// </summary> public abstract string Name { get; } /// <summary> /// Gets the source URL for the feed /// </summary> protected abstract string[] Urls { get; } public abstract bool IsConfigured { get; } /// <summary> /// Should the indexer be enabled by default? /// </summary> public virtual bool EnabledByDefault { get { return false; } } /// <summary> /// Gets the credential. /// </summary> protected virtual NetworkCredential Credentials { get { return null; } } protected abstract IList<String> GetEpisodeSearchUrls(string seriesTitle, int seasonNumber, int episodeNumber); protected abstract IList<String> GetDailyEpisodeSearchUrls(string seriesTitle, DateTime date); protected abstract IList<String> GetSeasonSearchUrls(string seriesTitle, int seasonNumber); protected abstract IList<String> GetPartialSeasonSearchUrls(string seriesTitle, int seasonNumber, int episodeWildcard); /// <summary> /// This method can be overwritten to provide indexer specific info parsing /// </summary> /// <param name="item">RSS item that needs to be parsed</param> /// <param name="currentResult">Result of the built in parse function.</param> /// <returns></returns> protected virtual EpisodeParseResult CustomParser(SyndicationItem item, EpisodeParseResult currentResult) { return currentResult; } /// <summary> /// This method can be overwritten to provide pre-parse the title /// </summary> /// <param name="item">RSS item that needs to be parsed</param> /// <returns></returns> protected virtual string TitlePreParser(SyndicationItem item) { return item.Title.Text; } /// <summary> /// Generates direct link to download an NZB /// </summary> /// <param name = "item">RSS Feed item to generate the link for</param> /// <returns>Download link URL</returns> protected abstract string NzbDownloadUrl(SyndicationItem item); /// <summary> /// Generates link to the NZB info at the indexer /// </summary> /// <param name = "item">RSS Feed item to generate the link for</param> /// <returns>Nzb Info URL</returns> protected abstract string NzbInfoUrl(SyndicationItem item); /// <summary> /// Fetches RSS feed and process each news item. /// </summary> public virtual IList<EpisodeParseResult> FetchRss() { _logger.Debug("Fetching feeds from " + Name); var result = new List<EpisodeParseResult>(); result = Fetch(Urls); _logger.Debug("Finished processing feeds from " + Name); return result; } public virtual IList<EpisodeParseResult> FetchSeason(string seriesTitle, int seasonNumber) { _logger.Debug("Searching {0} for {1} Season {2}", Name, seriesTitle, seasonNumber); var searchUrls = GetSeasonSearchUrls(GetQueryTitle(seriesTitle), seasonNumber); var result = Fetch(searchUrls); _logger.Info("Finished searching {0} for {1} Season {2}, Found {3}", Name, seriesTitle, seasonNumber, result.Count); return result; } public virtual IList<EpisodeParseResult> FetchPartialSeason(string seriesTitle, int seasonNumber, int episodePrefix) { _logger.Debug("Searching {0} for {1} Season {2}, Prefix: {3}", Name, seriesTitle, seasonNumber, episodePrefix); var searchUrls = GetPartialSeasonSearchUrls(GetQueryTitle(seriesTitle), seasonNumber, episodePrefix); var result = Fetch(searchUrls); _logger.Info("Finished searching {0} for {1} Season {2}, Found {3}", Name, seriesTitle, seasonNumber, result.Count); return result; } public virtual IList<EpisodeParseResult> FetchEpisode(string seriesTitle, int seasonNumber, int episodeNumber) { _logger.Debug("Searching {0} for {1}-S{2:00}E{3:00}", Name, seriesTitle, seasonNumber, episodeNumber); var searchUrls = GetEpisodeSearchUrls(GetQueryTitle(seriesTitle), seasonNumber, episodeNumber); var result = Fetch(searchUrls); _logger.Info("Finished searching {0} for {1} S{2:00}E{3:00}, Found {4}", Name, seriesTitle, seasonNumber, episodeNumber, result.Count); return result; } public virtual IList<EpisodeParseResult> FetchDailyEpisode(string seriesTitle, DateTime airDate) { _logger.Debug("Searching {0} for {1}-{2}", Name, seriesTitle, airDate.ToShortDateString()); var searchUrls = GetDailyEpisodeSearchUrls(GetQueryTitle(seriesTitle), airDate); var result = Fetch(searchUrls); _logger.Info("Finished searching {0} for {1}-{2}, Found {3}", Name, seriesTitle, airDate.ToShortDateString(), result.Count); return result; } private List<EpisodeParseResult> Fetch(IEnumerable<string> urls) { var result = new List<EpisodeParseResult>(); if (!IsConfigured) { _logger.Warn("Indexer '{0}' isn't configured correctly. please reconfigure the indexer in settings page.", Name); return result; } foreach (var url in urls) { try { _logger.Trace("Downloading RSS " + url); var reader = new SyndicationFeedXmlReader(_httpProvider.DownloadStream(url, Credentials)); var feed = SyndicationFeed.Load(reader).Items; foreach (var item in feed) { try { var parsedEpisode = ParseFeed(item); if (parsedEpisode != null) { parsedEpisode.NzbUrl = NzbDownloadUrl(item); parsedEpisode.NzbInfoUrl = NzbInfoUrl(item); parsedEpisode.Indexer = String.IsNullOrWhiteSpace(parsedEpisode.Indexer) ? Name : parsedEpisode.Indexer; result.Add(parsedEpisode); } } catch (Exception itemEx) { itemEx.Data.Add("FeedUrl", url); itemEx.Data.Add("Item", item.Title); _logger.ErrorException("An error occurred while processing feed item", itemEx); } } } catch (WebException webException) { if (webException.Message.Contains("503")) { _logger.Warn("{0} server is currently unavailable.{1} {2}", Name,url, webException.Message); } else { webException.Data.Add("FeedUrl", url); _logger.ErrorException("An error occurred while processing feed. " + url, webException); } } catch (Exception feedEx) { feedEx.Data.Add("FeedUrl", url); _logger.ErrorException("An error occurred while processing feed. " + url, feedEx); } } return result; } /// <summary> /// Parses the RSS feed item /// </summary> /// <param name = "item">RSS feed item to parse</param> /// <returns>Detailed episode info</returns> public EpisodeParseResult ParseFeed(SyndicationItem item) { var title = TitlePreParser(item); var episodeParseResult = Parser.ParseTitle(title); if (episodeParseResult != null) { episodeParseResult.Age = DateTime.Now.Date.Subtract(item.PublishDate.Date).Days; episodeParseResult.OriginalString = title; episodeParseResult.SceneSource = true; } _logger.Trace("Parsed: {0} from: {1}", episodeParseResult, item.Title.Text); return CustomParser(item, episodeParseResult); } /// <summary> /// This method can be overwritten to provide indexer specific title cleaning /// </summary> /// <param name="title">Title that needs to be cleaned</param> /// <returns></returns> public virtual string GetQueryTitle(string title) { title = RemoveThe.Replace(title, string.Empty); var cleanTitle = TitleSearchRegex.Replace(title, "+").Trim('+', ' '); //remove any repeating +s cleanTitle = Regex.Replace(cleanTitle, @"\+{1,100}", "+"); return cleanTitle; } } }