mirror of https://github.com/lidarr/Lidarr
Merge branch 'special-episode-search' of https://github.com/iaddis/NzbDrone into special-episode-search
Conflicts: src/NzbDrone.Core/Indexers/Omgwtfnzbs/Omgwtfnzbs.cs
This commit is contained in:
commit
faa24c5bb6
|
@ -63,6 +63,15 @@ namespace NzbDrone.Core.DecisionEngine
|
|||
{
|
||||
var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title);
|
||||
|
||||
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode())
|
||||
{
|
||||
var specialEpisodeInfo = _parsingService.ParseSpecialEpisodeTitle(report.Title, report.TvRageId, searchCriteria);
|
||||
if (specialEpisodeInfo != null)
|
||||
{
|
||||
parsedEpisodeInfo = specialEpisodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedEpisodeInfo != null && !string.IsNullOrWhiteSpace(parsedEpisodeInfo.SeriesTitle))
|
||||
{
|
||||
var remoteEpisode = _parsingService.Map(parsedEpisodeInfo, report.TvRageId, searchCriteria);
|
||||
|
|
|
@ -23,7 +23,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
|
|||
}
|
||||
}
|
||||
|
||||
private static string GetQueryTitle(string title)
|
||||
public static string GetQueryTitle(string title)
|
||||
{
|
||||
Ensure.That(title,() => title).IsNotNullOrWhiteSpace();
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch.Definitions
|
||||
{
|
||||
public class SpecialEpisodeSearchCriteria : SearchCriteriaBase
|
||||
{
|
||||
public string[] EpisodeQueryTitles { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("[{0} : {1}]", SceneTitle, String.Join(",", EpisodeQueryTitles));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -64,6 +64,12 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
return SearchDaily(series, episode);
|
||||
}
|
||||
|
||||
if (episode.SeasonNumber == 0)
|
||||
{
|
||||
// search for special episodes in season 0
|
||||
return SearchSpecial(series, new List<Episode>{episode});
|
||||
}
|
||||
|
||||
return SearchSingle(series, episode);
|
||||
}
|
||||
|
||||
|
@ -103,11 +109,28 @@ namespace NzbDrone.Core.IndexerSearch
|
|||
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
||||
}
|
||||
|
||||
private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episodes)
|
||||
{
|
||||
var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes);
|
||||
// build list of queries for each episode in the form: "<series> <episode-title>"
|
||||
searchSpec.EpisodeQueryTitles = episodes.Where(e => !String.IsNullOrWhiteSpace(e.Title))
|
||||
.Select(e => searchSpec.QueryTitle + " " + SearchCriteriaBase.GetQueryTitle(e.Title))
|
||||
.ToArray();
|
||||
|
||||
return Dispatch(indexer => _feedFetcher.Fetch(indexer, searchSpec), searchSpec);
|
||||
}
|
||||
|
||||
public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber)
|
||||
{
|
||||
var series = _seriesService.GetSeries(seriesId);
|
||||
var episodes = _episodeService.GetEpisodesBySeason(seriesId, seasonNumber);
|
||||
|
||||
if (seasonNumber == 0)
|
||||
{
|
||||
// search for special episodes in season 0
|
||||
return SearchSpecial(series, episodes);
|
||||
}
|
||||
|
||||
var searchSpec = Get<SeasonSearchCriteria>(series, episodes);
|
||||
searchSpec.SeasonNumber = seasonNumber;
|
||||
|
||||
|
|
|
@ -54,5 +54,10 @@ namespace NzbDrone.Core.Indexers.Eztv
|
|||
//EZTV doesn't support searching based on actual episode airdate. they only support release date.
|
||||
return new string[0];
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetSearchUrls(string query, int offset)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,5 +14,6 @@ namespace NzbDrone.Core.Indexers
|
|||
IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
|
||||
IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
|
||||
IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
|
||||
IEnumerable<string> GetSearchUrls(string query, int offset = 0);
|
||||
}
|
||||
}
|
|
@ -50,6 +50,7 @@ namespace NzbDrone.Core.Indexers
|
|||
public abstract IEnumerable<string> GetEpisodeSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int episodeNumber);
|
||||
public abstract IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date);
|
||||
public abstract IEnumerable<string> GetSeasonSearchUrls(string seriesTitle, int tvRageId, int seasonNumber, int offset);
|
||||
public abstract IEnumerable<string> GetSearchUrls(string query, int offset);
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace NzbDrone.Core.Indexers
|
|||
IList<ReleaseInfo> Fetch(IIndexer indexer, SeasonSearchCriteria searchCriteria);
|
||||
IList<ReleaseInfo> Fetch(IIndexer indexer, SingleEpisodeSearchCriteria searchCriteria);
|
||||
IList<ReleaseInfo> Fetch(IIndexer indexer, DailyEpisodeSearchCriteria searchCriteria);
|
||||
IList<ReleaseInfo> Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria);
|
||||
}
|
||||
|
||||
public class FetchFeedService : IFetchFeedFromIndexers
|
||||
|
@ -76,9 +77,8 @@ namespace NzbDrone.Core.Indexers
|
|||
|
||||
var searchUrls = indexer.GetEpisodeSearchUrls(searchCriteria.QueryTitle, searchCriteria.Series.TvRageId, searchCriteria.SeasonNumber, searchCriteria.EpisodeNumber);
|
||||
var result = Fetch(indexer, searchUrls);
|
||||
|
||||
|
||||
_logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -93,6 +93,20 @@ namespace NzbDrone.Core.Indexers
|
|||
return result;
|
||||
}
|
||||
|
||||
public IList<ReleaseInfo> Fetch(IIndexer indexer, SpecialEpisodeSearchCriteria searchCriteria)
|
||||
{
|
||||
var queryUrls = new List<String>();
|
||||
foreach (var episodeQueryTitle in searchCriteria.EpisodeQueryTitles)
|
||||
{
|
||||
_logger.Debug("Performing query of {0} for {1}", indexer, episodeQueryTitle);
|
||||
queryUrls.AddRange(indexer.GetSearchUrls(episodeQueryTitle));
|
||||
}
|
||||
|
||||
var result = Fetch(indexer, queryUrls);
|
||||
_logger.Info("Finished searching {0} for {1}. Found {2}", indexer, searchCriteria, result.Count);
|
||||
return result;
|
||||
}
|
||||
|
||||
private List<ReleaseInfo> Fetch(IIndexer indexer, IEnumerable<string> urls)
|
||||
{
|
||||
var result = new List<ReleaseInfo>();
|
||||
|
|
|
@ -111,6 +111,15 @@ namespace NzbDrone.Core.Indexers.Newznab
|
|||
return RecentFeed.Select(url => String.Format("{0}&limit=100&q={1}&season={2}&ep={3}", url, NewsnabifyTitle(seriesTitle), seasonNumber, episodeNumber));
|
||||
}
|
||||
|
||||
public override IEnumerable<string> 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 IEnumerable<string> GetDailyEpisodeSearchUrls(string seriesTitle, int tvRageId, DateTime date)
|
||||
{
|
||||
if (tvRageId > 0)
|
||||
|
|
|
@ -67,8 +67,13 @@ namespace NzbDrone.Core.Indexers.Omgwtfnzbs
|
|||
return searchUrls;
|
||||
}
|
||||
|
||||
public override bool SupportsPaging
|
||||
public override IEnumerable<string> GetSearchUrls(string query, int offset)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public override bool SupportsPaging
|
||||
{
|
||||
get
|
||||
{
|
||||
return false;
|
||||
|
|
|
@ -49,5 +49,10 @@ namespace NzbDrone.Core.Indexers.Wombles
|
|||
{
|
||||
return new List<string>();
|
||||
}
|
||||
|
||||
public override IEnumerable<string> GetSearchUrls(string query, int offset)
|
||||
{
|
||||
return new List<string>();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -289,6 +289,7 @@
|
|||
<Compile Include="Housekeeping\HousekeepingCommand.cs" />
|
||||
<Compile Include="Housekeeping\HousekeepingService.cs" />
|
||||
<Compile Include="Housekeeping\IHousekeepingTask.cs" />
|
||||
<Compile Include="IndexerSearch\Definitions\SpecialEpisodeSearchCriteria.cs" />
|
||||
<Compile Include="IndexerSearch\SeriesSearchService.cs" />
|
||||
<Compile Include="IndexerSearch\SeriesSearchCommand.cs" />
|
||||
<Compile Include="IndexerSearch\EpisodeSearchService.cs" />
|
||||
|
|
|
@ -34,6 +34,17 @@ namespace NzbDrone.Core.Parser.Model
|
|||
return AbsoluteEpisodeNumbers.Any();
|
||||
}
|
||||
|
||||
public bool IsPossibleSpecialEpisode()
|
||||
{
|
||||
// if we dont have eny episode numbers we are likely a special episode and need to do a search by episode title
|
||||
return string.IsNullOrEmpty(AirDate) &&
|
||||
(
|
||||
EpisodeNumbers.Length == 0 ||
|
||||
SeasonNumber == 0 ||
|
||||
String.IsNullOrWhiteSpace(SeriesTitle)
|
||||
);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
string episodeString = "[Unknown Episode]";
|
||||
|
|
|
@ -114,6 +114,14 @@ namespace NzbDrone.Core.Parser
|
|||
private static readonly Regex YearInTitleRegex = new Regex(@"^(?<title>.+?)(?:\W|_)?(?<year>\d{4})",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex WordDelimiterRegex = new Regex(@"(\s|\.|,|_|-|=|\|)+", RegexOptions.Compiled);
|
||||
private static readonly Regex PunctuationRegex = new Regex(@"[^\w\s]", RegexOptions.Compiled);
|
||||
private static readonly Regex CommonWordRegex = new Regex(@"\b(a|an|the|and|or|of)\b\s?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex SpecialEpisodeWordRegex = new Regex(@"\b(part|special|edition)\b\s?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
|
||||
public static ParsedEpisodeInfo ParsePath(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
@ -219,6 +227,15 @@ namespace NzbDrone.Core.Parser
|
|||
return MultiPartCleanupRegex.Replace(title, string.Empty).Trim();
|
||||
}
|
||||
|
||||
public static string NormalizeEpisodeTitle(string title)
|
||||
{
|
||||
string singleSpaces = WordDelimiterRegex.Replace(title, " ");
|
||||
string noPunctuation = PunctuationRegex.Replace(singleSpaces, String.Empty);
|
||||
string noCommonWords = CommonWordRegex.Replace(noPunctuation, String.Empty);
|
||||
string normalized = SpecialEpisodeWordRegex.Replace(noCommonWords, String.Empty);
|
||||
return normalized.Trim().ToLower();
|
||||
}
|
||||
|
||||
public static string ParseReleaseGroup(string title)
|
||||
{
|
||||
const string defaultReleaseGroup = "DRONE";
|
||||
|
|
|
@ -13,6 +13,8 @@ namespace NzbDrone.Core.Parser
|
|||
{
|
||||
public interface IParsingService
|
||||
{
|
||||
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null);
|
||||
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series);
|
||||
LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource);
|
||||
Series GetSeries(string title);
|
||||
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvRageId, SearchCriteriaBase searchCriteria = null);
|
||||
|
@ -40,10 +42,82 @@ namespace NzbDrone.Core.Parser
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvRageId, SearchCriteriaBase searchCriteria = null)
|
||||
{
|
||||
if (searchCriteria != null)
|
||||
{
|
||||
var tvdbId = _sceneMappingService.GetTvDbId(title);
|
||||
if (tvdbId.HasValue)
|
||||
{
|
||||
if (searchCriteria.Series.TvdbId == tvdbId)
|
||||
{
|
||||
return ParseSpecialEpisodeTitle(title, searchCriteria.Series);
|
||||
}
|
||||
}
|
||||
|
||||
if (tvRageId == searchCriteria.Series.TvRageId)
|
||||
{
|
||||
return ParseSpecialEpisodeTitle(title, searchCriteria.Series);
|
||||
}
|
||||
}
|
||||
|
||||
var series = _seriesService.FindByTitleInexact(title);
|
||||
if (series == null && tvRageId > 0)
|
||||
{
|
||||
series = _seriesService.FindByTvRageId(tvRageId);
|
||||
}
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
_logger.Trace("No matching series {0}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
return ParseSpecialEpisodeTitle(title, series);
|
||||
}
|
||||
|
||||
public ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, Series series)
|
||||
{
|
||||
// find special episode in series season 0
|
||||
var episode = _episodeService.FindEpisodeByName(series.Id, 0, title);
|
||||
if (episode != null)
|
||||
{
|
||||
// create parsed info from tv episode
|
||||
var info = new ParsedEpisodeInfo();
|
||||
info.SeriesTitle = series.Title;
|
||||
info.SeriesTitleInfo = new SeriesTitleInfo();
|
||||
info.SeriesTitleInfo.Title = info.SeriesTitle;
|
||||
info.SeasonNumber = episode.SeasonNumber;
|
||||
info.EpisodeNumbers = new int[1] { episode.EpisodeNumber };
|
||||
info.FullSeason = false;
|
||||
info.Quality = QualityParser.ParseQuality(title);
|
||||
info.ReleaseGroup = Parser.ParseReleaseGroup(title);
|
||||
|
||||
_logger.Info("Found special episode {0} for title '{1}'", info, title);
|
||||
return info;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
public LocalEpisode GetEpisodes(string filename, Series series, bool sceneSource)
|
||||
{
|
||||
var parsedEpisodeInfo = Parser.ParsePath(filename);
|
||||
|
||||
// do we have a possible special episode?
|
||||
if (parsedEpisodeInfo == null || parsedEpisodeInfo.IsPossibleSpecialEpisode())
|
||||
{
|
||||
// try to parse as a special episode
|
||||
var title = System.IO.Path.GetFileNameWithoutExtension(filename);
|
||||
var specialEpisodeInfo = ParseSpecialEpisodeTitle(title, series);
|
||||
if (specialEpisodeInfo != null)
|
||||
{
|
||||
// use special episode
|
||||
parsedEpisodeInfo = specialEpisodeInfo;
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedEpisodeInfo == null)
|
||||
{
|
||||
return null;
|
||||
|
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.Tv
|
|||
Episode GetEpisode(int id);
|
||||
Episode FindEpisode(int seriesId, int seasonNumber, int episodeNumber, bool useScene = false);
|
||||
Episode FindEpisode(int seriesId, int absoluteEpisodeNumber);
|
||||
Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle);
|
||||
Episode GetEpisode(int seriesId, String date);
|
||||
Episode FindEpisode(int seriesId, String date);
|
||||
List<Episode> GetEpisodeBySeries(int seriesId);
|
||||
|
@ -88,6 +89,21 @@ namespace NzbDrone.Core.Tv
|
|||
return _episodeRepository.GetEpisodes(seriesId, seasonNumber);
|
||||
}
|
||||
|
||||
public Episode FindEpisodeByName(int seriesId, int seasonNumber, string episodeTitle)
|
||||
{
|
||||
// TODO: can replace this search mechanism with something smarter/faster/better
|
||||
var search = Parser.Parser.NormalizeEpisodeTitle(episodeTitle);
|
||||
return _episodeRepository.GetEpisodes(seriesId, seasonNumber)
|
||||
.FirstOrDefault(e =>
|
||||
{
|
||||
// normalize episode title
|
||||
string title = Parser.Parser.NormalizeEpisodeTitle(e.Title);
|
||||
// find episode title within search string
|
||||
return (title.Length > 0) && search.Contains(title);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
public PagingSpec<Episode> EpisodesWithoutFiles(PagingSpec<Episode> pagingSpec)
|
||||
{
|
||||
var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, false);
|
||||
|
|
|
@ -20,6 +20,7 @@ namespace NzbDrone.Core.Tv
|
|||
Series FindByTvRageId(int tvRageId);
|
||||
Series FindByTitle(string title);
|
||||
Series FindByTitle(string title, int year);
|
||||
Series FindByTitleInexact(string title);
|
||||
void SetSeriesType(int seriesId, SeriesTypes seriesTypes);
|
||||
void DeleteSeries(int seriesId, bool deleteFiles);
|
||||
List<Series> GetAllSeries();
|
||||
|
@ -102,6 +103,51 @@ namespace NzbDrone.Core.Tv
|
|||
return _seriesRepository.FindByTitle(Parser.Parser.CleanSeriesTitle(title));
|
||||
}
|
||||
|
||||
public Series FindByTitleInexact(string title)
|
||||
{
|
||||
// find any series clean title within the provided release title
|
||||
string cleanTitle = Parser.Parser.CleanSeriesTitle(title);
|
||||
var list = _seriesRepository.All().Where(s => cleanTitle.Contains(s.CleanTitle)).ToList();
|
||||
if (!list.Any())
|
||||
{
|
||||
// no series matched
|
||||
return null;
|
||||
}
|
||||
else if (list.Count == 1)
|
||||
{
|
||||
// return the first series if there is only one
|
||||
return list.Single();
|
||||
}
|
||||
else
|
||||
{
|
||||
// build ordered list of series by position in the search string
|
||||
var query =
|
||||
list.Select(series => new
|
||||
{
|
||||
position = cleanTitle.IndexOf(series.CleanTitle),
|
||||
length = series.CleanTitle.Length,
|
||||
series = series
|
||||
})
|
||||
.Where(s => (s.position>=0))
|
||||
.ToList()
|
||||
.OrderBy(s => s.position)
|
||||
.ThenByDescending(s => s.length)
|
||||
.ToList();
|
||||
|
||||
// get the leftmost series that is the longest
|
||||
// series are usually the first thing in release title, so we select the leftmost and longest match
|
||||
var match = query.First().series;
|
||||
|
||||
_logger.Trace("Multiple series matched {0} from title {1}", match.Title, title);
|
||||
foreach (var entry in list)
|
||||
{
|
||||
_logger.Trace("Multiple series match candidate: {0} cleantitle: {1}", entry.Title, entry.CleanTitle);
|
||||
}
|
||||
|
||||
return match;
|
||||
}
|
||||
}
|
||||
|
||||
public Series FindByTitle(string title, int year)
|
||||
{
|
||||
return _seriesRepository.FindByTitle(title, year);
|
||||
|
|
Loading…
Reference in New Issue