parent
84cbfe870f
commit
cfb1a80c58
@ -0,0 +1,55 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { value }) => value,
|
||||
(state) => state.indexers,
|
||||
(value, indexers) => {
|
||||
const values = indexers.items.map(({ id, name }) => {
|
||||
return {
|
||||
key: id,
|
||||
value: name
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
value,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class IndexersSelectInputConnector extends Component {
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexersSelectInputConnector.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
indexerIds: PropTypes.number.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onChange: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps)(IndexersSelectInputConnector);
|
@ -0,0 +1,6 @@
|
||||
.message {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './NoSearchResults.css';
|
||||
|
||||
function NoSearchResults(props) {
|
||||
const { totalItems } = props;
|
||||
|
||||
if (totalItems > 0) {
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
{translate('AllIndexersHiddenDueToFilter')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={styles.message}>
|
||||
No search results found, try performing a new search below.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
NoSearchResults.propTypes = {
|
||||
totalItems: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
export default NoSearchResults;
|
@ -1,74 +0,0 @@
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import { set, update } from './baseActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'movieTitles';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_MOVIE_TITLES = 'movieTitles/fetchMovieTitles';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchMovieTitles = createThunk(FETCH_MOVIE_TITLES);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_MOVIE_TITLES]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/alttitle',
|
||||
data: payload
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
}, defaultState, section);
|
@ -1,11 +0,0 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class MoviesSearchCommand : Command
|
||||
{
|
||||
public string SearchTerm { get; set; }
|
||||
|
||||
public override bool SendUpdatesToClient => true;
|
||||
}
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Instrumentation.Extensions;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class MovieSearchService : IExecute<MoviesSearchCommand>
|
||||
{
|
||||
private readonly ISearchForNzb _nzbSearchService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public MovieSearchService(ISearchForNzb nzbSearchService,
|
||||
Logger logger)
|
||||
{
|
||||
_nzbSearchService = nzbSearchService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void Execute(MoviesSearchCommand message)
|
||||
{
|
||||
var decisions = _nzbSearchService.MovieSearch(message.SearchTerm, false, false);
|
||||
|
||||
_logger.ProgressInfo("Movie search completed. {0} reports downloaded.", decisions.Count);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class NewznabRequest
|
||||
{
|
||||
public int id { get; set; }
|
||||
public string t { get; set; }
|
||||
public string q { get; set; }
|
||||
public string cat { get; set; }
|
||||
public string imdbid { get; set; }
|
||||
public string tmdbid { get; set; }
|
||||
public string extended { get; set; }
|
||||
public string limit { get; set; }
|
||||
public string offset { get; set; }
|
||||
public string rid { get; set; }
|
||||
public string tvdbid { get; set; }
|
||||
public string season { get; set; }
|
||||
public string ep { get; set; }
|
||||
public string album { get; set; }
|
||||
public string artist { get; set; }
|
||||
public string label { get; set; }
|
||||
public string track { get; set; }
|
||||
public string year { get; set; }
|
||||
public string genre { get; set; }
|
||||
public string author { get; set; }
|
||||
public string title { get; set; }
|
||||
public string configured { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Xml.Linq;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.IndexerSearch
|
||||
{
|
||||
public class NewznabResults
|
||||
{
|
||||
private static readonly XNamespace _AtomNs = "http://www.w3.org/2005/Atom";
|
||||
private static readonly XNamespace _TorznabNs = "http://torznab.com/schemas/2015/feed";
|
||||
|
||||
// filters control characters but allows only properly-formed surrogate sequences
|
||||
// https://stackoverflow.com/a/961504
|
||||
private static readonly Regex _InvalidXmlChars = new Regex(
|
||||
@"(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|[\x00-\x08\x0B\x0C\x0E-\x1F\x7F-\x9F\uFEFF\uFFFE\uFFFF]",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
public List<ReleaseInfo> Releases;
|
||||
|
||||
private static string RemoveInvalidXMLChars(string text)
|
||||
{
|
||||
if (text == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _InvalidXmlChars.Replace(text, "");
|
||||
}
|
||||
|
||||
private static string XmlDateFormat(DateTime dt)
|
||||
{
|
||||
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
|
||||
|
||||
//Sat, 14 Mar 2015 17:10:42 -0400
|
||||
return $"{dt:ddd, dd MMM yyyy HH:mm:ss} " + $"{dt:zzz}".Replace(":", "");
|
||||
}
|
||||
|
||||
private static XElement GetTorznabElement(string name, object value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new XElement(_TorznabNs + "attr", new XAttribute("name", name), new XAttribute("value", value));
|
||||
}
|
||||
|
||||
public string ToXml()
|
||||
{
|
||||
// IMPORTANT: We can't use Uri.ToString(), because it generates URLs without URL encode (links with unicode
|
||||
// characters are broken). We must use Uri.AbsoluteUri instead that handles encoding correctly
|
||||
var xdoc = new XDocument(
|
||||
new XDeclaration("1.0", "UTF-8", null),
|
||||
new XElement("rss",
|
||||
new XAttribute("version", "1.0"),
|
||||
new XAttribute(XNamespace.Xmlns + "atom", _AtomNs.NamespaceName),
|
||||
new XAttribute(XNamespace.Xmlns + "torznab", _TorznabNs.NamespaceName),
|
||||
new XElement("channel",
|
||||
new XElement(_AtomNs + "link",
|
||||
new XAttribute("rel", "self"),
|
||||
new XAttribute("type", "application/rss+xml")),
|
||||
new XElement("title", "Prowlarr"),
|
||||
from r in Releases
|
||||
let t = (r as TorrentInfo) ?? new TorrentInfo()
|
||||
select new XElement("item",
|
||||
new XElement("title", RemoveInvalidXMLChars(r.Title)),
|
||||
new XElement("guid", r.Guid), // GUID and (Link or Magnet) are mandatory
|
||||
new XElement("prowlarrindexer", new XAttribute("id", r.IndexerId), r.Indexer),
|
||||
r.PublishDate == DateTime.MinValue ? new XElement("pubDate", XmlDateFormat(DateTime.Now)) : new XElement("pubDate", XmlDateFormat(r.PublishDate)),
|
||||
new XElement("size", r.Size),
|
||||
new XElement(
|
||||
"enclosure",
|
||||
new XAttribute("length", r.Size),
|
||||
new XAttribute("type", "application/x-bittorrent")),
|
||||
GetTorznabElement("rageid", r.TvRageId),
|
||||
GetTorznabElement("thetvdb", r.TvdbId),
|
||||
GetTorznabElement("imdb", r.ImdbId.ToString("D7")),
|
||||
GetTorznabElement("tmdb", r.TmdbId),
|
||||
GetTorznabElement("seeders", t.Seeders),
|
||||
GetTorznabElement("peers", t.Peers),
|
||||
GetTorznabElement("infohash", RemoveInvalidXMLChars(r.Guid))))));
|
||||
|
||||
return xdoc.Declaration + Environment.NewLine + xdoc;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,33 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NzbDrone.Core.Indexers.Newznab
|
||||
{
|
||||
public class NewznabCapabilities
|
||||
{
|
||||
public int DefaultPageSize { get; set; }
|
||||
public int MaxPageSize { get; set; }
|
||||
public string[] SupportedSearchParameters { get; set; }
|
||||
public string[] SupportedMovieSearchParameters { get; set; }
|
||||
public bool SupportsAggregateIdSearch { get; set; }
|
||||
public List<NewznabCategory> Categories { get; set; }
|
||||
|
||||
public NewznabCapabilities()
|
||||
{
|
||||
DefaultPageSize = 100;
|
||||
MaxPageSize = 100;
|
||||
SupportedSearchParameters = new[] { "q" };
|
||||
SupportedMovieSearchParameters = new[] { "q", "imdbid", "imdbtitle", "imdbyear" };
|
||||
SupportsAggregateIdSearch = false;
|
||||
Categories = new List<NewznabCategory>();
|
||||
}
|
||||
}
|
||||
|
||||
public class NewznabCategory
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Description { get; set; }
|
||||
|
||||
public List<NewznabCategory> Subcategories { get; set; }
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using NzbDrone.Core.Languages;
|
||||
|
||||
namespace NzbDrone.Core.Parser.Model
|
||||
{
|
||||
public class ParsedMovieInfo
|
||||
{
|
||||
public string MovieTitle { get; set; }
|
||||
public string OriginalTitle { get; set; }
|
||||
public string ReleaseTitle { get; set; }
|
||||
public string SimpleReleaseTitle { get; set; }
|
||||
public List<Language> Languages { get; set; } = new List<Language>();
|
||||
public string ReleaseGroup { get; set; }
|
||||
public string ReleaseHash { get; set; }
|
||||
public string Edition { get; set; }
|
||||
public int Year { get; set; }
|
||||
public string ImdbId { get; set; }
|
||||
[JsonIgnore]
|
||||
public Dictionary<string, object> ExtraInfo { get; set; } = new Dictionary<string, object>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return string.Format("{0} - {1}", MovieTitle, Year);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,584 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Common.Instrumentation;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
#if !LIBRARY
|
||||
#endif
|
||||
|
||||
namespace NzbDrone.Core.Parser
|
||||
{
|
||||
public static class Parser
|
||||
{
|
||||
private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(Parser));
|
||||
|
||||
private static readonly Regex ReportYearRegex = new Regex(@"^.*(?<year>(19|20)\d{2}).*$", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex EditionRegex = new Regex(@"\(?\b(?<edition>(((Recut.|Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\b\)?", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex ReportEditionRegex = new Regex(@"^.+?" + EditionRegex, RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly Regex[] ReportMovieTitleRegex = new[]
|
||||
{
|
||||
//Some german or french tracker formats (missing year, ...) (Only applies to german and French/TrueFrench releases) - see ParserFixture for examples and tests
|
||||
new Regex(@"^(?<title>(?![(\[]).+?)((\W|_))(" + EditionRegex + @".{1,3})?(?:(?<!(19|20)\d{2}.*?)(German|French|TrueFrench))(.+?)(?=((19|20)\d{2}|$))(?<year>(19|20)\d{2}(?!p|i|\d+|\]|\W\d+))?(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.Special.Edition.2011
|
||||
new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*" + EditionRegex + @".{1,3}(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//Special, Despecialized, etc. Edition Movies, e.g: Mission.Impossible.3.2011.Special.Edition //TODO: Seems to slow down parsing heavily!
|
||||
/*new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(19|20)\d{2}(?!p|i|(19|20)\d{2}|\]|\W(19|20)\d{2})))+(\W+|_|$)(?!\\)\(?(?<edition>(((Extended.|Ultimate.)?(Director.?s|Collector.?s|Theatrical|Ultimate|Final(?=(.(Cut|Edition|Version)))|Extended|Rogue|Special|Despecialized|\d{2,3}(th)?.Anniversary)(.(Cut|Edition|Version))?(.(Extended|Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit))?|((Uncensored|Remastered|Unrated|Uncut|IMAX|Fan.?Edit|Edition|Restored|((2|3|4)in1))))))\)?",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled),*/
|
||||
|
||||
//Normal movie format, e.g: Mission.Impossible.3.2011
|
||||
new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|(1(8|9)|20)\d{2}|\]|\W(1(8|9)|20)\d{2})))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//PassThePopcorn Torrent names: Star.Wars[PassThePopcorn]
|
||||
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![()\[!]))*(?<year>(\[\w *\])))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//That did not work? Maybe some tool uses [] for years. Who would do that?
|
||||
new Regex(@"^(?<title>(?![(\[]).+?)?(?:(?:[-_\W](?<![)!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
|
||||
//As a last resort for movies that have ( or [ in their title.
|
||||
new Regex(@"^(?<title>.+?)?(?:(?:[-_\W](?<![)\[!]))*(?<year>(1(8|9)|20)\d{2}(?!p|i|\d+|\]|\W\d+)))+(\W+|_|$)(?!\\)", RegexOptions.IgnoreCase | RegexOptions.Compiled),
|
||||
};
|
||||
|
||||
private static readonly Regex[] ReportMovieTitleFolderRegex = new[]
|
||||
{
|
||||
//When year comes first.
|
||||
new Regex(@"^(?:(?:[-_\W](?<![)!]))*(?<year>(19|20)\d{2}(?!p|i|\d+|\W\d+)))+(\W+|_|$)(?<title>.+?)?$")
|
||||
};
|
||||
|
||||
private static readonly Regex[] RejectHashedReleasesRegex = new Regex[]
|
||||
{
|
||||
// Generic match for md5 and mixed-case hashes.
|
||||
new Regex(@"^[0-9a-zA-Z]{32}", RegexOptions.Compiled),
|
||||
|
||||
// Generic match for shorter lower-case hashes.
|
||||
new Regex(@"^[a-z0-9]{24}$", RegexOptions.Compiled),
|
||||
|
||||
// Format seen on some NZBGeek releases
|
||||
// Be very strict with these coz they are very close to the valid 101 ep numbering.
|
||||
new Regex(@"^[A-Z]{11}\d{3}$", RegexOptions.Compiled),
|
||||
new Regex(@"^[a-z]{12}\d{3}$", RegexOptions.Compiled),
|
||||
|
||||
//Backup filename (Unknown origins)
|
||||
new Regex(@"^Backup_\d{5,}S\d{2}-\d{2}$", RegexOptions.Compiled),
|
||||
|
||||
//123 - Started appearing December 2014
|
||||
new Regex(@"^123$", RegexOptions.Compiled),
|
||||
|
||||
//abc - Started appearing January 2015
|
||||
new Regex(@"^abc$", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
//abc - Started appearing 2020
|
||||
new Regex(@"^abc[-_. ]xyz", RegexOptions.Compiled | RegexOptions.IgnoreCase),
|
||||
|
||||
//b00bs - Started appearing January 2015
|
||||
new Regex(@"^b00bs$", RegexOptions.Compiled | RegexOptions.IgnoreCase)
|
||||
};
|
||||
|
||||
//Regex to detect whether the title was reversed.
|
||||
private static readonly Regex ReversedTitleRegex = new Regex(@"(?:^|[-._ ])(p027|p0801)[-._ ]", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex NormalizeRegex = new Regex(@"((?:\b|_)(?<!^|[^a-zA-Z0-9_']\w[^a-zA-Z0-9_'])(a(?!$|[^a-zA-Z0-9_']\w[^a-zA-Z0-9_'])|an|the|and|or|of)(?:\b|_))|\W|_",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex FileExtensionRegex = new Regex(@"\.[a-z0-9]{2,4}$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex ReportImdbId = new Regex(@"(?<imdbid>tt\d{7,8})", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly RegexReplace SimpleTitleRegex = new RegexReplace(@"\s*(?:480[ip]|576[ip]|720[ip]|1080[ip]|2160[ip]|[xh][\W_]?26[45]|DD\W?5\W1|[<>?*:|]|848x480|1280x720|1920x1080|(8|10)b(it)?)",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex SimpleReleaseTitleRegex = new Regex(@"\s*(?:[<>?*:|])", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
|
||||
private static readonly RegexReplace WebsitePrefixRegex = new RegexReplace(@"^\[\s*[-a-z]+(\.[a-z]+)+\s*\][- ]*|^www\.[a-z]+\.(?:com|net|org)[ -]*",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly RegexReplace WebsitePostfixRegex = new RegexReplace(@"\[\s*[-a-z]+(\.[a-z0-9]+)+\s*\]$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly RegexReplace CleanReleaseGroupRegex = new RegexReplace(@"(-(RP|1|NZBGeek|Obfuscated|Obfuscation|Scrambled|sample|Pre|postbot|xpost|Rakuv[a-z0-9]*|WhiteRev|BUYMORE|AsRequested|AlternativeToRequested|GEROV|Z0iDS3N|Chamele0n|4P|4Planet|AlteZachen))+$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly RegexReplace CleanTorrentSuffixRegex = new RegexReplace(@"\[(?:ettv|rartv|rarbg|cttv)\]$",
|
||||
string.Empty,
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex CleanQualityBracketsRegex = new Regex(@"\[[a-z0-9 ._-]+\]$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex ReleaseGroupRegex = new Regex(@"-(?<releasegroup>[a-z0-9]+(?!.+?(?:480p|720p|1080p|2160p)))(?<!.*?WEB-DL|Blu-Ray|480p|720p|1080p|2160p|DTS-HD|DTS-X|DTS-MA|DTS-ES)(?:\b|[-._ ]|$)|[-._ ]\[(?<releasegroup>[a-z0-9]+)\]$",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex AnimeReleaseGroupRegex = new Regex(@"^(?:\[(?<subgroup>(?!\s).+?(?<!\s))\](?:_|-|\s|\.)?)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
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 SpecialCharRegex = new Regex(@"(\&|\:|\\|\/)+", 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|christmas)\b\s?", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
private static readonly Regex DuplicateSpacesRegex = new Regex(@"\s{2,}", RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex RequestInfoRegex = new Regex(@"^(?:\[.+?\])+", RegexOptions.Compiled);
|
||||
|
||||
private static readonly string[] Numbers = new[] { "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine" };
|
||||
private static Dictionary<string, string> _umlautMappings = new Dictionary<string, string>
|
||||
{
|
||||
{ "ö", "oe" },
|
||||
{ "ä", "ae" },
|
||||
{ "ü", "ue" },
|
||||
};
|
||||
|
||||
public static ParsedMovieInfo ParseMoviePath(string path)
|
||||
{
|
||||
var fileInfo = new FileInfo(path);
|
||||
|
||||
var result = ParseMovieTitle(fileInfo.Name, true);
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Debug("Attempting to parse movie info using directory and file names. {0}", fileInfo.Directory.Name);
|
||||
result = ParseMovieTitle(fileInfo.Directory.Name + " " + fileInfo.Name);
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
Logger.Debug("Attempting to parse movie info using directory name. {0}", fileInfo.Directory.Name);
|
||||
result = ParseMovieTitle(fileInfo.Directory.Name + fileInfo.Extension);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static ParsedMovieInfo ParseMovieTitle(string title, bool isDir = false)
|
||||
{
|
||||
var originalTitle = title;
|
||||
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 releaseTitle = RemoveFileExtension(title);
|
||||
|
||||
//Trim dashes from end
|
||||
releaseTitle = releaseTitle.Trim('-', '_');
|
||||
|
||||
releaseTitle = releaseTitle.Replace("【", "[").Replace("】", "]");
|
||||
|
||||
var simpleTitle = SimpleTitleRegex.Replace(releaseTitle);
|
||||
|
||||
// TODO: Quick fix stripping [url] - prefixes.
|
||||
simpleTitle = WebsitePrefixRegex.Replace(simpleTitle);
|
||||
simpleTitle = WebsitePostfixRegex.Replace(simpleTitle);
|
||||
|
||||
simpleTitle = CleanTorrentSuffixRegex.Replace(simpleTitle);
|
||||
|
||||
var allRegexes = ReportMovieTitleRegex.ToList();
|
||||
|
||||
if (isDir)
|
||||
{
|
||||
allRegexes.AddRange(ReportMovieTitleFolderRegex);
|
||||
}
|
||||
|
||||
foreach (var regex in allRegexes)
|
||||
{
|
||||
var match = regex.Matches(simpleTitle);
|
||||
|
||||
if (match.Count != 0)
|
||||
{
|
||||
Logger.Trace(regex);
|
||||
try
|
||||
{
|
||||
var result = ParseMovieMatchCollection(match);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
//TODO: Add tests for this!
|
||||
var simpleReleaseTitle = SimpleReleaseTitleRegex.Replace(releaseTitle, string.Empty);
|
||||
|
||||
var simpleTitleReplaceString = match[0].Groups["title"].Success ? match[0].Groups["title"].Value : result.MovieTitle;
|
||||
|
||||
if (simpleTitleReplaceString.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
simpleReleaseTitle = simpleReleaseTitle.Replace(simpleTitleReplaceString, simpleTitleReplaceString.Contains(".") ? "A.Movie" : "A Movie");
|
||||
}
|
||||
|
||||
result.ReleaseGroup = ParseReleaseGroup(simpleReleaseTitle);
|
||||
|
||||
var subGroup = GetSubGroup(match);
|
||||
if (!subGroup.IsNullOrWhiteSpace())
|
||||
{
|
||||
result.ReleaseGroup = subGroup;
|
||||
}
|
||||
|
||||
Logger.Debug("Release Group parsed: {0}", result.ReleaseGroup);
|
||||
|
||||
result.Languages = LanguageParser.ParseLanguages(result.ReleaseGroup.IsNotNullOrWhiteSpace() ? simpleReleaseTitle.Replace(result.ReleaseGroup, "RlsGrp") : simpleReleaseTitle);
|
||||
Logger.Debug("Languages parsed: {0}", string.Join(", ", result.Languages));
|
||||
|
||||
if (result.Edition.IsNullOrWhiteSpace())
|
||||
{
|
||||
result.Edition = ParseEdition(simpleReleaseTitle);
|
||||
Logger.Debug("Edition parsed: {0}", result.Edition);
|
||||
}
|
||||
|
||||
result.ReleaseHash = GetReleaseHash(match);
|
||||
if (!result.ReleaseHash.IsNullOrWhiteSpace())
|
||||
{
|
||||
Logger.Debug("Release Hash parsed: {0}", result.ReleaseHash);
|
||||
}
|
||||
|
||||
result.OriginalTitle = originalTitle;
|
||||
result.ReleaseTitle = releaseTitle;
|
||||
result.SimpleReleaseTitle = simpleReleaseTitle;
|
||||
|
||||
result.ImdbId = ParseImdbId(simpleReleaseTitle);
|
||||
|
||||
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 {0}", title);
|
||||
}
|
||||
}
|
||||
|
||||
Logger.Debug("Unable to parse {0}", title);
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ParseImdbId(string title)
|
||||
{
|
||||
var match = ReportImdbId.Match(title);
|
||||
if (match.Success)
|
||||
{
|
||||
if (match.Groups["imdbid"].Value != null)
|
||||
{
|
||||
if (match.Groups["imdbid"].Length == 9 || match.Groups["imdbid"].Length == 10)
|
||||
{
|
||||
return match.Groups["imdbid"].Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static string ParseEdition(string languageTitle)
|
||||
{
|
||||
var editionMatch = ReportEditionRegex.Match(languageTitle);
|
||||
|
||||
if (editionMatch.Success && editionMatch.Groups["edition"].Value != null &&
|
||||
editionMatch.Groups["edition"].Value.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
return editionMatch.Groups["edition"].Value.Replace(".", " ");
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
public static string ReplaceGermanUmlauts(string s)
|
||||
{
|
||||
var t = s;
|
||||
t = t.Replace("ä", "ae");
|
||||
t = t.Replace("ö", "oe");
|
||||
t = t.Replace("ü", "ue");
|
||||
t = t.Replace("Ä", "Ae");
|
||||
t = t.Replace("Ö", "Oe");
|
||||
t = t.Replace("Ü", "Ue");
|
||||
t = t.Replace("ß", "ss");
|
||||
return t;
|
||||
}
|
||||
|
||||
public static string NormalizeImdbId(string imdbId)
|
||||
{
|
||||
if (imdbId.Length > 2)
|
||||
{
|
||||
imdbId = imdbId.Replace("tt", "").PadLeft(7, '0');
|
||||
return $"tt{imdbId}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string ToUrlSlug(string value)
|
||||
{
|
||||
//First to lower case
|
||||
value = value.ToLowerInvariant();
|
||||
|
||||
//Remove all accents
|
||||
value = value.RemoveAccent();
|
||||
|
||||
//Replace spaces
|
||||
value = Regex.Replace(value, @"\s", "-", RegexOptions.Compiled);
|
||||
|
||||
//Remove invalid chars
|
||||
value = Regex.Replace(value, @"[^a-z0-9\s-_]", "", RegexOptions.Compiled);
|
||||
|
||||
//Trim dashes from end
|
||||
value = value.Trim('-', '_');
|
||||
|
||||
//Replace double occurences of - or _
|
||||
value = Regex.Replace(value, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
public static string CleanMovieTitle(this string title)
|
||||
{
|
||||
long number = 0;
|
||||
|
||||
//If Title only contains numbers return it as is.
|
||||
if (long.TryParse(title, out number))
|
||||
{
|
||||
return title;
|
||||
}
|
||||
|
||||
return ReplaceGermanUmlauts(NormalizeRegex.Replace(title, string.Empty).ToLower()).RemoveAccent();
|
||||
}
|
||||
|
||||
public static string NormalizeEpisodeTitle(this string title)
|
||||
{
|
||||
title = SpecialEpisodeWordRegex.Replace(title, string.Empty);
|
||||
title = PunctuationRegex.Replace(title, " ");
|
||||
title = DuplicateSpacesRegex.Replace(title, " ");
|
||||
|
||||
return title.Trim()
|
||||
.ToLower();
|
||||
}
|
||||
|
||||
public static string NormalizeTitle(this string title)
|
||||
{
|
||||
title = WordDelimiterRegex.Replace(title, " ");
|
||||
title = PunctuationRegex.Replace(title, string.Empty);
|
||||
title = CommonWordRegex.Replace(title, string.Empty);
|
||||
title = DuplicateSpacesRegex.Replace(title, " ");
|
||||
title = SpecialCharRegex.Replace(title, string.Empty);
|
||||
|
||||
return title.Trim().ToLower();
|
||||
}
|
||||
|
||||
public static string SimplifyReleaseTitle(this string title)
|
||||
{
|
||||
return SimpleReleaseTitleRegex.Replace(title, string.Empty);
|
||||
}
|
||||
|
||||
public static string ParseReleaseGroup(string title)
|
||||
{
|
||||
title = title.Trim();
|
||||
title = RemoveFileExtension(title);
|
||||
title = WebsitePrefixRegex.Replace(title);
|
||||
|
||||
var animeMatch = AnimeReleaseGroupRegex.Match(title);
|
||||
|
||||
if (animeMatch.Success)
|
||||
{
|
||||
return animeMatch.Groups["subgroup"].Value;
|
||||
}
|
||||
|
||||
title = CleanReleaseGroupRegex.Replace(title);
|
||||
|
||||
var matches = ReleaseGroupRegex.Matches(title);
|
||||
|
||||
if (matches.Count != 0)
|
||||
{
|
||||
var group = matches.OfType<Match>().Last().Groups["releasegroup"].Value;
|
||||
int groupIsNumeric;
|
||||
|
||||
if (int.TryParse(group, out groupIsNumeric))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return group;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static string RemoveFileExtension(string title)
|
||||
{
|
||||
title = FileExtensionRegex.Replace(title, m =>
|
||||
{
|
||||
var extension = m.Value.ToLower();
|
||||
|
||||
return m.Value;
|
||||
});
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
private static ParsedMovieInfo ParseMovieMatchCollection(MatchCollection matchCollection)
|
||||
{
|
||||
if (!matchCollection[0].Groups["title"].Success || matchCollection[0].Groups["title"].Value == "(")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var movieName = matchCollection[0].Groups["title"].Value.Replace('_', ' ');
|
||||
movieName = RequestInfoRegex.Replace(movieName, "").Trim(' ');
|
||||
|
||||
var parts = movieName.Split('.');
|
||||
movieName = "";
|
||||
int n = 0;
|
||||
bool previousAcronym = false;
|
||||
string nextPart = "";
|
||||
foreach (var part in parts)
|
||||
{
|
||||
if (parts.Length >= n + 2)
|
||||
{
|
||||
nextPart = parts[n + 1];
|
||||
}
|
||||
else
|
||||
{
|
||||
nextPart = "";
|
||||
}
|
||||
|
||||
if (part.Length == 1 && part.ToLower() != "a" && !int.TryParse(part, out _) &&
|
||||
(previousAcronym || n < parts.Length - 1) &&
|
||||
(previousAcronym || nextPart.Length != 1 || !int.TryParse(nextPart, out _)))
|
||||
{
|
||||
movieName += part + ".";
|
||||
previousAcronym = true;
|
||||
}
|
||||
else if (part.ToLower() == "a" && (previousAcronym || nextPart.Length == 1))
|
||||
{
|
||||
movieName += part + ".";
|
||||
previousAcronym = true;
|
||||
}
|
||||
else if (part.ToLower() == "dr")
|
||||
{
|
||||
movieName += part + ".";
|
||||
previousAcronym = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (previousAcronym)
|
||||
{
|
||||
movieName += " ";
|
||||
previousAcronym = false;
|
||||
}
|
||||
|
||||
movieName += part + " ";
|
||||
}
|
||||
|
||||
n++;
|
||||
}
|
||||
|
||||
movieName = movieName.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 = movieName;
|
||||
|
||||
Logger.Debug("Movie Parsed. {0}", result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool ValidateBeforeParsing(string title)
|
||||
{
|
||||
if (title.ToLower().Contains("password") && title.ToLower().Contains("yenc"))
|
||||
{
|
||||
Logger.Debug("");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!title.Any(char.IsLetterOrDigit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var titleWithoutExtension = RemoveFileExtension(title);
|
||||
|
||||
if (RejectHashedReleasesRegex.Any(v => v.IsMatch(titleWithoutExtension)))
|
||||
{
|
||||
Logger.Debug("Rejected Hashed Release Title: " + title);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string GetSubGroup(MatchCollection matchCollection)
|
||||
{
|
||||
var subGroup = matchCollection[0].Groups["subgroup"];
|
||||
|
||||
if (subGroup.Success)
|
||||
{
|
||||
return subGroup.Value;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private static string GetReleaseHash(MatchCollection matchCollection)
|
||||
{
|
||||
var hash = matchCollection[0].Groups["hash"];
|
||||
|
||||
if (hash.Success)
|
||||
{
|
||||
var hashValue = hash.Value.Trim('[', ']');
|
||||
|
||||
if (hashValue.Equals("1280x720"))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return hashValue;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
namespace NzbDrone.Core.Parser
|
||||
{
|
||||
public static class SceneChecker
|
||||
{
|
||||
//This method should prefer false negatives over false positives.
|
||||
//It's better not to use a title that might be scene than to use one that isn't scene
|
||||
public static string GetSceneTitle(string title)
|
||||
{
|
||||
if (title == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!title.Contains("."))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (title.Contains(" "))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parsedTitle = Parser.ParseMovieTitle(title);
|
||||
|
||||
if (parsedTitle == null ||
|
||||
parsedTitle.ReleaseGroup == null ||
|
||||
string.IsNullOrWhiteSpace(parsedTitle.MovieTitle) ||
|
||||
string.IsNullOrWhiteSpace(parsedTitle.ReleaseTitle))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return parsedTitle.ReleaseTitle;
|
||||
}
|
||||
|
||||
public static bool IsSceneTitle(string title)
|
||||
{
|
||||
return GetSceneTitle(title) != null;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue