Updated Parser to parse movie titles. Should also parse things, such as: Director's Cut, Special Edition, etc. This is then displayed in the manual search UI. Importing is not yet updated for the new parser!

This commit is contained in:
Leonardo Galli 2017-01-04 22:59:34 +01:00
parent 402a9e1ee0
commit fd718b61ac
14 changed files with 348 additions and 23 deletions

View File

@ -24,6 +24,7 @@ namespace NzbDrone.Api.Indexers
public string Indexer { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public string Edition { get; set; }
public string Title { get; set; }
public bool FullSeason { get; set; }
public int SeasonNumber { get; set; }
@ -90,6 +91,55 @@ namespace NzbDrone.Api.Indexers
if (model.IsForMovie)
{
downloadAllowed = model.RemoteMovie.DownloadAllowed;
var parsedMovieInfo = model.RemoteMovie.ParsedMovieInfo;
return new ReleaseResource
{
Guid = releaseInfo.Guid,
Quality = parsedMovieInfo.Quality,
//QualityWeight
Age = releaseInfo.Age,
AgeHours = releaseInfo.AgeHours,
AgeMinutes = releaseInfo.AgeMinutes,
Size = releaseInfo.Size,
IndexerId = releaseInfo.IndexerId,
Indexer = releaseInfo.Indexer,
ReleaseGroup = parsedMovieInfo.ReleaseGroup,
ReleaseHash = parsedMovieInfo.ReleaseHash,
Title = releaseInfo.Title,
FullSeason = parsedMovieInfo.FullSeason,
SeasonNumber = parsedMovieInfo.SeasonNumber,
Language = parsedMovieInfo.Language,
AirDate = "",
SeriesTitle = parsedMovieInfo.MovieTitle,
EpisodeNumbers = new int[0],
AbsoluteEpisodeNumbers = new int[0],
Approved = model.Approved,
TemporarilyRejected = model.TemporarilyRejected,
Rejected = model.Rejected,
TvdbId = releaseInfo.TvdbId,
TvRageId = releaseInfo.TvRageId,
Rejections = model.Rejections.Select(r => r.Reason).ToList(),
PublishDate = releaseInfo.PublishDate,
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
DownloadAllowed = downloadAllowed,
//ReleaseWeight
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,
Seeders = torrentInfo.Seeders,
Leechers = (torrentInfo.Peers.HasValue && torrentInfo.Seeders.HasValue) ? (torrentInfo.Peers.Value - torrentInfo.Seeders.Value) : (int?)null,
Protocol = releaseInfo.DownloadProtocol,
Edition = parsedMovieInfo.Edition,
IsDaily = false,
IsAbsoluteNumbering = false,
IsPossibleSpecialEpisode = false,
Special = parsedMovieInfo.Special,
};
}
// TODO: Clean this mess up. don't mix data from multiple classes, use sub-resources instead? (Got a huge Deja Vu, didn't we talk about this already once?)

View File

@ -66,9 +66,9 @@ namespace NzbDrone.Core.DecisionEngine
try
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(report.Title);
var parsedEpisodeInfo = Parser.Parser.ParseMovieTitle(report.Title);
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.SeriesTitle.IsNullOrWhiteSpace())
if (parsedEpisodeInfo != null && !parsedEpisodeInfo.MovieTitle.IsNullOrWhiteSpace())
{
RemoteMovie remoteEpisode = _parsingService.Map(parsedEpisodeInfo, "", searchCriteria);
remoteEpisode.Release = report;

View File

@ -57,12 +57,13 @@ namespace NzbDrone.Core.Download.TrackedDownloads
try
{
var parsedEpisodeInfo = Parser.Parser.ParseTitle(trackedDownload.DownloadItem.Title);
var parsedMovieInfo = Parser.Parser.ParseMovieTitle(trackedDownload.DownloadItem.Title);
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId);
if (parsedEpisodeInfo != null)
{
trackedDownload.RemoteEpisode = _parsingService.Map(parsedEpisodeInfo, 0, 0);
trackedDownload.RemoteMovie = _parsingService.Map(parsedEpisodeInfo, "", null);
trackedDownload.RemoteMovie = _parsingService.Map(parsedMovieInfo, "", null);
}
if (historyItems.Any())

View File

@ -893,6 +893,7 @@
<Compile Include="Parser\IsoLanguages.cs" />
<Compile Include="Parser\LanguageParser.cs" />
<Compile Include="Parser\Model\LocalMovie.cs" />
<Compile Include="Parser\Model\ParsedMovieInfo.cs" />
<Compile Include="Parser\Model\RemoteMovie.cs" />
<Compile Include="Profiles\Delay\DelayProfile.cs" />
<Compile Include="Profiles\Delay\DelayProfileService.cs" />

View File

@ -0,0 +1,31 @@
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Qualities;
namespace NzbDrone.Core.Parser.Model
{
public class ParsedMovieInfo
{
public string MovieTitle { get; set; }
public SeriesTitleInfo MovieTitleInfo { get; set; }
public QualityModel Quality { get; set; }
public int SeasonNumber { get; set; }
public Language Language { get; set; }
public bool FullSeason { get; set; }
public bool Special { get; set; }
public string ReleaseGroup { get; set; }
public string ReleaseHash { get; set; }
public string Edition { get; set;}
public int Year { get; set; }
public ParsedMovieInfo()
{
}
public override string ToString()
{
return string.Format("{0} - {1} {2}", MovieTitle, MovieTitleInfo.Year, Quality);
}
}
}

View File

@ -9,6 +9,7 @@ namespace NzbDrone.Core.Parser.Model
{
public ReleaseInfo Release { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } //TODO: Change to ParsedMovieInfo, for now though ParsedEpisodeInfo will do.
public ParsedMovieInfo ParsedMovieInfo { get; set; }
public Movie Movie { get; set; }
public bool DownloadAllowed { get; set; }

View File

@ -15,6 +15,26 @@ namespace NzbDrone.Core.Parser
{
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser));
private static readonly Regex[] ReportMovieTitleRegex = new[]
{
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.Special.Edition.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<edition>(\w+\.?edition))\.(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)(?<edition>(\w+\.?edition))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Cut Movies, e.g: Mission.Impossible.3.Directors.Cut.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<edition>(\w+\.?cut))\.(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Cut Movies, e.g: Mission.Impossible.3.2011.Directors.Cut
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)(?<edition>(\w+\.?cut))",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
//Normal movie format, e.g: Mission.Impossible.3.2011
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(?<!e|x)\d{4}(?!p|i|\d+|\)|\]|\W\d+)))+(\W+|_|$)(?!\\)",
RegexOptions.IgnoreCase | RegexOptions.Compiled),
};
private static readonly Regex[] ReportTitleRegex = new[]
{
//Anime - Absolute Episode Number + Title + Season+Episode
@ -298,6 +318,94 @@ namespace NzbDrone.Core.Parser
return result;
}
public static ParsedMovieInfo ParseMovieTitle(string title)
{
ParsedMovieInfo realResult = null;
try
{
if (!ValidateBeforeParsing(title)) return null;
Logger.Debug("Parsing string '{0}'", title);
if (ReversedTitleRegex.IsMatch(title))
{
var titleWithoutExtension = RemoveFileExtension(title).ToCharArray();
Array.Reverse(titleWithoutExtension);
title = new string(titleWithoutExtension) + title.Substring(titleWithoutExtension.Length);
Logger.Debug("Reversed name detected. Converted to '{0}'", title);
}
var simpleTitle = SimpleTitleRegex.Replace(title, string.Empty);
simpleTitle = RemoveFileExtension(simpleTitle);
// TODO: Quick fix stripping [url] - prefixes.
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle, string.Empty);
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle, string.Empty);
foreach (var regex in ReportMovieTitleRegex)
{
var match = regex.Matches(simpleTitle);
if (match.Count != 0)
{
Logger.Trace(regex);
try
{
var result = ParseMovieMatchCollection(match);
if (result != null)
{
result.Language = LanguageParser.ParseLanguage(title);
Logger.Debug("Language parsed: {0}", result.Language);
result.Quality = QualityParser.ParseQuality(title);
Logger.Debug("Quality parsed: {0}", result.Quality);
result.ReleaseGroup = ParseReleaseGroup(title);
var subGroup = GetSubGroup(match);
if (!subGroup.IsNullOrWhiteSpace())
{
result.ReleaseGroup = subGroup;
}
Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup);
result.ReleaseHash = GetReleaseHash(match);
if (!result.ReleaseHash.IsNullOrWhiteSpace())
{
Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash);
}
realResult = result;
return result;
}
}
catch (InvalidDateException ex)
{
Logger.Debug(ex, ex.Message);
break;
}
}
}
}
catch (Exception e)
{
if (!title.ToLower().Contains("password") && !title.ToLower().Contains("yenc"))
Logger.Error(e, "An error has occurred while trying to parse " + title);
}
Logger.Debug("Unable to parse {0}", title);
return realResult;
}
public static ParsedEpisodeInfo ParseTitle(string title)
{
@ -528,6 +636,31 @@ namespace NzbDrone.Core.Parser
return seriesTitleInfo;
}
private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection)
{
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');
seriesName = RequestInfoRegex.Replace(seriesName, "").Trim(' ');
int airYear;
int.TryParse(matchCollection[0].Groups["year"].Value, out airYear);
ParsedMovieInfo result;
result = new ParsedMovieInfo { Year = airYear };
if (matchCollection[0].Groups["edition"].Success)
{
result.Edition = matchCollection[0].Groups["edition"].Value.Replace(".", " ");
}
result.MovieTitle = seriesName;
result.MovieTitleInfo = GetSeriesTitleInfo(result.MovieTitle);
Logger.Debug("Movie Parsed. {0}", result);
return result;
}
private static ParsedEpisodeInfo ParseMatchCollection(MatchCollection matchCollection)
{
var seriesName = matchCollection[0].Groups["title"].Value.Replace('.', ' ').Replace('_', ' ');

View File

@ -21,7 +21,7 @@ namespace NzbDrone.Core.Parser
Movie GetMovie(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
RemoteMovie Map(ParsedMovieInfo parsedMovieInfo, string imdbId, SearchCriteriaBase searchCriteria = null);
List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null);
ParsedEpisodeInfo ParseSpecialEpisodeTitle(string title, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
}
@ -33,6 +33,20 @@ namespace NzbDrone.Core.Parser
private readonly ISceneMappingService _sceneMappingService;
private readonly IMovieService _movieService;
private readonly Logger _logger;
private readonly Dictionary<string, string> romanNumeralsMapper = new Dictionary<string, string>
{
{ "1", "I"},
{ "2", "II"},
{ "3", "III"},
{ "4", "IV"},
{ "5", "V"},
{ "6", "VI"},
{ "7", "VII"},
{ "8", "VII"},
{ "9", "IX"},
{ "10", "X"},
}; //If a movie has more than 10 parts fuck 'em.
public ParsingService(IEpisodeService episodeService,
ISeriesService seriesService,
@ -163,19 +177,19 @@ namespace NzbDrone.Core.Parser
public Movie GetMovie(string title)
{
var parsedEpisodeInfo = Parser.ParseTitle(title);
var parsedEpisodeInfo = Parser.ParseMovieTitle(title);
if (parsedEpisodeInfo == null)
{
return _movieService.FindByTitle(title);
}
var series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
var series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle);
if (series == null)
{
series = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitleInfo.TitleWithoutYear,
parsedEpisodeInfo.SeriesTitleInfo.Year);
series = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitleInfo.TitleWithoutYear,
parsedEpisodeInfo.MovieTitleInfo.Year);
}
return series;
@ -201,11 +215,11 @@ namespace NzbDrone.Core.Parser
return remoteEpisode;
}
public RemoteMovie Map(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null)
public RemoteMovie Map(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria = null)
{
var remoteEpisode = new RemoteMovie
{
ParsedEpisodeInfo = parsedEpisodeInfo,
ParsedMovieInfo = parsedEpisodeInfo,
};
var movie = GetMovie(parsedEpisodeInfo, imdbId, searchCriteria);
@ -334,23 +348,46 @@ namespace NzbDrone.Core.Parser
return null;
}
private Movie GetMovie(ParsedEpisodeInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria)
private Movie GetMovie(ParsedMovieInfo parsedEpisodeInfo, string imdbId, SearchCriteriaBase searchCriteria)
{
if (searchCriteria != null)
{
if (searchCriteria.Movie.CleanTitle == parsedEpisodeInfo.SeriesTitle.CleanSeriesTitle())
var possibleTitles = new List<string>();
possibleTitles.Add(searchCriteria.Movie.CleanTitle);
foreach (string altTitle in searchCriteria.Movie.AlternativeTitles)
{
return searchCriteria.Movie;
possibleTitles.Add(altTitle.CleanSeriesTitle());
}
if (imdbId.IsNotNullOrWhiteSpace() && imdbId == searchCriteria.Movie.ImdbId)
foreach (string title in possibleTitles)
{
//TODO: If series is found by TvdbId, we should report it as a scene naming exception, since it will fail to import
return searchCriteria.Movie;
if (title == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
foreach (KeyValuePair<string, string> entry in romanNumeralsMapper)
{
string num = entry.Key;
string roman = entry.Value.ToLower();
if (title.Replace(num, roman) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
if (title.Replace(roman, num) == parsedEpisodeInfo.MovieTitle.CleanSeriesTitle())
{
return searchCriteria.Movie;
}
}
}
}
Movie movie = _movieService.FindByTitle(parsedEpisodeInfo.SeriesTitle);
Movie movie = _movieService.FindByTitle(parsedEpisodeInfo.MovieTitle); //Todo: same as above!
if (movie == null && imdbId.IsNotNullOrWhiteSpace())
{
@ -360,7 +397,7 @@ namespace NzbDrone.Core.Parser
if (movie == null)
{
_logger.Debug("No matching movie {0}", parsedEpisodeInfo.SeriesTitle);
_logger.Debug("No matching movie {0}", parsedEpisodeInfo.MovieTitle);
return null;
}

View File

@ -0,0 +1,41 @@
var Backgrid = require('backgrid');
var Marionette = require('marionette');
require('bootstrap');
module.exports = Backgrid.Cell.extend({
className : 'edition-cell',
//template : 'Cells/EditionCellTemplate',
render : function() {
var edition = this.model.get(this.column.get('name'));
if (!edition) {
return this;
}
var cut = false;
if (edition.toLowerCase().contains("cut")) {
cut = true;
}
//this.templateFunction = Marionette.TemplateCache.get(this.template);
//var html = this.templateFunction(edition);
if (cut) {
this.$el.html('<i class="icon-sonarr-form-cut"/ title="{0}">'.format(edition));
} else {
this.$el.html('<i class="icon-sonarr-form-info"/ title="{0}">'.format(edition));
}
/*this.$el.popover({
content : html,
html : true,
trigger : 'hover',
title : this.column.get('title'),
placement : 'left',
container : this.$el
});*/
return this;
}
});

View File

@ -0,0 +1,5 @@
<ul>
<li>
{{this}}
</li>
</ul>

View File

@ -121,6 +121,10 @@
.fa-icon-color(@brand-danger);
}
.icon-sonarr-form-cut {
.fa-icon-content(@fa-var-scissors);
}
.icon-sonarr-form-info-link {
.clickable();
.fa-icon-content(@fa-var-info-circle);
@ -502,4 +506,4 @@
.icon-sonarr-header-rejections {
.fa-icon-content(@fa-var-exclamation-circle);
}
}

View File

@ -8,6 +8,7 @@ var DownloadReportCell = require('../../Release/DownloadReportCell');
var AgeCell = require('../../Release/AgeCell');
var ProtocolCell = require('../../Release/ProtocolCell');
var PeersCell = require('../../Release/PeersCell');
var EditionCell = require('../../Cells/EditionCell');
module.exports = Marionette.Layout.extend({
template : 'Episode/Search/ManualLayoutTemplate',
@ -32,6 +33,12 @@ module.exports = Marionette.Layout.extend({
label : 'Title',
cell : ReleaseTitleCell
},
{
name : 'edition',
label : 'Edition',
cell : EditionCell,
title : "Edition"
},
{
name : 'indexer',
label : 'Indexer',
@ -83,4 +90,4 @@ module.exports = Marionette.Layout.extend({
}));
}
}
});
});

View File

@ -8,6 +8,7 @@ var DownloadReportCell = require('../../Release/DownloadReportCell');
var AgeCell = require('../../Release/AgeCell');
var ProtocolCell = require('../../Release/ProtocolCell');
var PeersCell = require('../../Release/PeersCell');
var EditionCell = require('../../Cells/EditionCell');
module.exports = Marionette.Layout.extend({
template : 'Movies/Search/ManualLayoutTemplate',
@ -32,6 +33,12 @@ module.exports = Marionette.Layout.extend({
label : 'Title',
cell : ReleaseTitleCell
},
{
name : 'edition',
label : 'Edition',
cell : EditionCell,
title : "Edition"
},
{
name : 'indexer',
label : 'Indexer',

View File

@ -7,6 +7,7 @@ var FileSizeCell = require('../Cells/FileSizeCell');
var QualityCell = require('../Cells/QualityCell');
var ApprovalStatusCell = require('../Cells/ApprovalStatusCell');
var LoadingView = require('../Shared/LoadingView');
var EditionCell = require('../Cells/EditionCell');
module.exports = Marionette.Layout.extend({
template : 'Release/ReleaseLayoutTemplate',
@ -17,6 +18,12 @@ module.exports = Marionette.Layout.extend({
},
columns : [
{
name : 'edition',
label : 'Edition',
sortable : false,
cell : EditionCell
},
{
name : 'indexer',
label : 'Indexer',
@ -29,12 +36,12 @@ module.exports = Marionette.Layout.extend({
sortable : true,
cell : Backgrid.StringCell
},
{
/*{
name : 'episodeNumbers',
episodes : 'episodeNumbers',
label : 'season',
cell : EpisodeNumberCell
},
},*/
{
name : 'size',
label : 'Size',
@ -75,4 +82,4 @@ module.exports = Marionette.Layout.extend({
}));
}
}
});
});