diff options
author | morpheus65535 <[email protected]> | 2018-09-16 20:27:00 -0400 |
---|---|---|
committer | morpheus65535 <[email protected]> | 2018-09-16 20:33:04 -0400 |
commit | 0f061f21226f91883c841f85ceef31b30981277a (patch) | |
tree | a1350723ae688ccbae4d4ca564cc4175ccc73996 /libs/guessit | |
parent | 8b681d8a151a3b41d3aaa5bfdd7a082bdda7896c (diff) | |
download | bazarr-0f061f21226f91883c841f85ceef31b30981277a.tar.gz bazarr-0f061f21226f91883c841f85ceef31b30981277a.zip |
Include dependencies and remove requirements.txt
Diffstat (limited to 'libs/guessit')
90 files changed, 19047 insertions, 0 deletions
diff --git a/libs/guessit/__init__.py b/libs/guessit/__init__.py new file mode 100644 index 000000000..365935eb0 --- /dev/null +++ b/libs/guessit/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Extracts as much information as possible from a video file. +""" +from .api import guessit, GuessItApi +from .options import ConfigurationException +from .rules.common.quantity import Size + +from .__version__ import __version__ diff --git a/libs/guessit/__main__.py b/libs/guessit/__main__.py new file mode 100644 index 000000000..3b4e815fa --- /dev/null +++ b/libs/guessit/__main__.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Entry point module +""" +# pragma: no cover +from __future__ import print_function + +import json +import logging +import os +import sys + +import six +from rebulk.__version__ import __version__ as __rebulk_version__ + +from guessit import api +from guessit.__version__ import __version__ +from guessit.jsonutils import GuessitEncoder +from guessit.options import argument_parser, parse_options, load_config + + +try: + from collections import OrderedDict +except ImportError: # pragma: no-cover + from ordereddict import OrderedDict # pylint:disable=import-error + + +def guess_filename(filename, options): + """ + Guess a single filename using given options + :param filename: filename to parse + :type filename: str + :param options: + :type options: dict + :return: + :rtype: + """ + if not options.get('yaml') and not options.get('json') and not options.get('show_property'): + print('For:', filename) + + guess = api.guessit(filename, options) + + if options.get('show_property'): + print(guess.get(options.get('show_property'), '')) + return + + if options.get('json'): + print(json.dumps(guess, cls=GuessitEncoder, ensure_ascii=False)) + elif options.get('yaml'): + import yaml + from guessit import yamlutils + + ystr = yaml.dump({filename: OrderedDict(guess)}, Dumper=yamlutils.CustomDumper, default_flow_style=False, + allow_unicode=True) + i = 0 + for yline in ystr.splitlines(): + if i == 0: + print("? " + yline[:-1]) + elif i == 1: + print(":" + yline[1:]) + else: + print(yline) + i += 1 + else: + print('GuessIt found:', json.dumps(guess, cls=GuessitEncoder, indent=4, ensure_ascii=False)) + + +def display_properties(options): + """ + Display properties + """ + properties = api.properties(options) + + if options.get('json'): + if options.get('values'): + print(json.dumps(properties, cls=GuessitEncoder, ensure_ascii=False)) + else: + print(json.dumps(list(properties.keys()), cls=GuessitEncoder, ensure_ascii=False)) + elif options.get('yaml'): + import yaml + from guessit import yamlutils + if options.get('values'): + print(yaml.dump(properties, Dumper=yamlutils.CustomDumper, default_flow_style=False, allow_unicode=True)) + else: + print(yaml.dump(list(properties.keys()), Dumper=yamlutils.CustomDumper, default_flow_style=False, + allow_unicode=True)) + else: + print('GuessIt properties:') + + properties_list = list(sorted(properties.keys())) + for property_name in properties_list: + property_values = properties.get(property_name) + print(2 * ' ' + '[+] %s' % (property_name,)) + if property_values and options.get('values'): + for property_value in property_values: + print(4 * ' ' + '[!] %s' % (property_value,)) + + +def main(args=None): # pylint:disable=too-many-branches + """ + Main function for entry point + """ + if six.PY2 and os.name == 'nt': # pragma: no cover + # see http://bugs.python.org/issue2128 + import locale + + for i, j in enumerate(sys.argv): + sys.argv[i] = j.decode(locale.getpreferredencoding()) + + if args is None: # pragma: no cover + options = parse_options() + else: + options = parse_options(args) + options = load_config(options) + if options.get('verbose'): + logging.basicConfig(stream=sys.stdout, format='%(message)s') + logging.getLogger().setLevel(logging.DEBUG) + + help_required = True + + if options.get('version'): + print('+-------------------------------------------------------+') + print('+ GuessIt ' + __version__ + (28 - len(__version__)) * ' ' + '+') + print('+-------------------------------------------------------+') + print('+ Rebulk ' + __rebulk_version__ + (29 - len(__rebulk_version__)) * ' ' + '+') + print('+-------------------------------------------------------+') + print('| Please report any bug or feature request at |') + print('| https://github.com/guessit-io/guessit/issues. |') + print('+-------------------------------------------------------+') + help_required = False + + if options.get('yaml'): + try: + import yaml # pylint:disable=unused-variable + except ImportError: # pragma: no cover + del options['yaml'] + print('PyYAML is not installed. \'--yaml\' option will be ignored ...', file=sys.stderr) + + if options.get('properties') or options.get('values'): + display_properties(options) + help_required = False + + filenames = [] + if options.get('filename'): + for filename in options.get('filename'): + filenames.append(filename) + if options.get('input_file'): + if six.PY2: + input_file = open(options.get('input_file'), 'r') + else: + input_file = open(options.get('input_file'), 'r', encoding='utf-8') + try: + filenames.extend([line.strip() for line in input_file.readlines()]) + finally: + input_file.close() + + filenames = list(filter(lambda f: f, filenames)) + + if filenames: + for filename in filenames: + help_required = False + guess_filename(filename, options) + + if help_required: # pragma: no cover + argument_parser.print_help() + + +if __name__ == '__main__': # pragma: no cover + main() diff --git a/libs/guessit/__version__.py b/libs/guessit/__version__.py new file mode 100644 index 000000000..4f64b2211 --- /dev/null +++ b/libs/guessit/__version__.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Version module +""" +# pragma: no cover +__version__ = '3.0.0' diff --git a/libs/guessit/api.py b/libs/guessit/api.py new file mode 100644 index 000000000..555b8d865 --- /dev/null +++ b/libs/guessit/api.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +API functions that can be used by external software +""" + +try: + from collections import OrderedDict +except ImportError: # pragma: no-cover + from ordereddict import OrderedDict # pylint:disable=import-error + +import os +import traceback + +import six + +from rebulk.introspector import introspect + +from .rules import rebulk_builder +from .options import parse_options, load_config +from .__version__ import __version__ + + +class GuessitException(Exception): + """ + Exception raised when guessit fails to perform a guess because of an internal error. + """ + def __init__(self, string, options): + super(GuessitException, self).__init__("An internal error has occured in guessit.\n" + "===================== Guessit Exception Report =====================\n" + "version=%s\n" + "string=%s\n" + "options=%s\n" + "--------------------------------------------------------------------\n" + "%s" + "--------------------------------------------------------------------\n" + "Please report at " + "https://github.com/guessit-io/guessit/issues.\n" + "====================================================================" % + (__version__, str(string), str(options), traceback.format_exc())) + + self.string = string + self.options = options + + +def configure(options, rules_builder=rebulk_builder): + """ + Load rebulk rules according to advanced configuration in options dictionary. + + :param options: + :type options: dict + :param rules_builder: + :type rules_builder: + :return: + """ + default_api.configure(options, rules_builder=rules_builder, force=True) + + +def guessit(string, options=None): + """ + Retrieves all matches from string as a dict + :param string: the filename or release name + :type string: str + :param options: + :type options: str|dict + :return: + :rtype: + """ + return default_api.guessit(string, options) + + +def properties(options=None): + """ + Retrieves all properties with possible values that can be guessed + :param options: + :type options: str|dict + :return: + :rtype: + """ + return default_api.properties(options) + + +class GuessItApi(object): + """ + An api class that can be configured with custom Rebulk configuration. + """ + + def __init__(self): + """Default constructor.""" + self.rebulk = None + + @classmethod + def _fix_encoding(cls, value): + if isinstance(value, list): + return [cls._fix_encoding(item) for item in value] + if isinstance(value, dict): + return {cls._fix_encoding(k): cls._fix_encoding(v) for k, v in value.items()} + if six.PY2 and isinstance(value, six.text_type): + return value.encode('utf-8') + if six.PY3 and isinstance(value, six.binary_type): + return value.decode('ascii') + return value + + def configure(self, options, rules_builder=rebulk_builder, force=False): + """ + Load rebulk rules according to advanced configuration in options dictionary. + + :param options: + :type options: str|dict + :param rules_builder: + :type rules_builder: + :param force: + :return: + :rtype: dict + """ + options = parse_options(options, True) + should_load = force or not self.rebulk + advanced_config = options.pop('advanced_config', None) + + if should_load and not advanced_config: + advanced_config = load_config(options)['advanced_config'] + + options = self._fix_encoding(options) + + if should_load: + advanced_config = self._fix_encoding(advanced_config) + self.rebulk = rules_builder(advanced_config) + + return options + + def guessit(self, string, options=None): # pylint: disable=too-many-branches + """ + Retrieves all matches from string as a dict + :param string: the filename or release name + :type string: str|Path + :param options: + :type options: str|dict + :return: + :rtype: + """ + try: + from pathlib import Path + if isinstance(string, Path): + try: + # Handle path-like object + string = os.fspath(string) + except AttributeError: + string = str(string) + except ImportError: + pass + + try: + options = self.configure(options) + result_decode = False + result_encode = False + + if six.PY2: + if isinstance(string, six.text_type): + string = string.encode("utf-8") + result_decode = True + elif isinstance(string, six.binary_type): + string = six.binary_type(string) + if six.PY3: + if isinstance(string, six.binary_type): + string = string.decode('ascii') + result_encode = True + elif isinstance(string, six.text_type): + string = six.text_type(string) + + matches = self.rebulk.matches(string, options) + if result_decode: + for match in matches: + if isinstance(match.value, six.binary_type): + match.value = match.value.decode("utf-8") + if result_encode: + for match in matches: + if isinstance(match.value, six.text_type): + match.value = match.value.encode("ascii") + return matches.to_dict(options.get('advanced', False), options.get('single_value', False), + options.get('enforce_list', False)) + except: + raise GuessitException(string, options) + + def properties(self, options=None): + """ + Grab properties and values that can be generated. + :param options: + :type options: + :return: + :rtype: + """ + options = self.configure(options) + unordered = introspect(self.rebulk, options).properties + ordered = OrderedDict() + for k in sorted(unordered.keys(), key=six.text_type): + ordered[k] = list(sorted(unordered[k], key=six.text_type)) + if hasattr(self.rebulk, 'customize_properties'): + ordered = self.rebulk.customize_properties(ordered) + return ordered + + +default_api = GuessItApi() diff --git a/libs/guessit/backports.py b/libs/guessit/backports.py new file mode 100644 index 000000000..3e94e27ad --- /dev/null +++ b/libs/guessit/backports.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Backports +""" +# pragma: no-cover +# pylint: disabled + +def cmp_to_key(mycmp): + """functools.cmp_to_key backport""" + class KeyClass(object): + """Key class""" + def __init__(self, obj, *args): # pylint: disable=unused-argument + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + return KeyClass diff --git a/libs/guessit/config/options.json b/libs/guessit/config/options.json new file mode 100644 index 000000000..0fe274b16 --- /dev/null +++ b/libs/guessit/config/options.json @@ -0,0 +1,363 @@ +{ + "expected_title": [ + "OSS 117" + ], + "allowed_countries": [ + "au", + "us", + "gb" + ], + "allowed_languages": [ + "de", + "en", + "es", + "ca", + "cs", + "fr", + "he", + "hi", + "hu", + "it", + "ja", + "ko", + "nl", + "pl", + "pt", + "ro", + "ru", + "sv", + "te", + "uk", + "mul", + "und" + ], + "advanced_config": { + "common_words": [ + "de", + "it" + ], + "groups": { + "starting": "([{", + "ending": ")]}" + }, + "container": { + "subtitles": [ + "srt", + "idx", + "sub", + "ssa", + "ass" + ], + "info": [ + "nfo" + ], + "videos": [ + "3g2", + "3gp", + "3gp2", + "asf", + "avi", + "divx", + "flv", + "m4v", + "mk2", + "mka", + "mkv", + "mov", + "mp4", + "mp4a", + "mpeg", + "mpg", + "ogg", + "ogm", + "ogv", + "qt", + "ra", + "ram", + "rm", + "ts", + "wav", + "webm", + "wma", + "wmv", + "iso", + "vob" + ], + "torrent": [ + "torrent" + ], + "nzb": [ + "nzb" + ] + }, + "country": { + "synonyms": { + "ES": [ + "españa" + ], + "GB": [ + "UK" + ], + "BR": [ + "brazilian", + "bra" + ], + "CA": [ + "québec", + "quebec", + "qc" + ], + "MX": [ + "Latinoamérica", + "latin america" + ] + } + }, + "episodes": { + "season_max_range": 100, + "episode_max_range": 100, + "max_range_gap": 1, + "season_markers": [ + "s" + ], + "season_ep_markers": [ + "x" + ], + "disc_markers": [ + "d" + ], + "episode_markers": [ + "xe", + "ex", + "ep", + "e", + "x" + ], + "range_separators": [ + "-", + "~", + "to", + "a" + ], + "discrete_separators": [ + "+", + "&", + "and", + "et" + ], + "season_words": [ + "season", + "saison", + "seizoen", + "serie", + "seasons", + "saisons", + "series", + "tem", + "temp", + "temporada", + "temporadas", + "stagione" + ], + "episode_words": [ + "episode", + "episodes", + "eps", + "ep", + "episodio", + "episodios", + "capitulo", + "capitulos" + ], + "of_words": [ + "of", + "sur" + ], + "all_words": [ + "All" + ] + }, + "language": { + "synonyms": { + "ell": [ + "gr", + "greek" + ], + "spa": [ + "esp", + "español", + "espanol" + ], + "fra": [ + "français", + "vf", + "vff", + "vfi", + "vfq" + ], + "swe": [ + "se" + ], + "por_BR": [ + "po", + "pb", + "pob", + "ptbr", + "br", + "brazilian" + ], + "deu_CH": [ + "swissgerman", + "swiss german" + ], + "nld_BE": [ + "flemish" + ], + "cat": [ + "català", + "castellano", + "espanol castellano", + "español castellano" + ], + "ces": [ + "cz" + ], + "ukr": [ + "ua" + ], + "zho": [ + "cn" + ], + "jpn": [ + "jp" + ], + "hrv": [ + "scr" + ], + "mul": [ + "multi", + "dl" + ] + }, + "subtitle_affixes": [ + "sub", + "subs", + "esub", + "esubs", + "subbed", + "custom subbed", + "custom subs", + "custom sub", + "customsubbed", + "customsubs", + "customsub", + "soft subtitles", + "soft subs" + ], + "subtitle_prefixes": [ + "st", + "v", + "vost", + "subforced", + "fansub", + "hardsub", + "legenda", + "legendas", + "legendado", + "subtitulado", + "soft", + "subtitles" + ], + "subtitle_suffixes": [ + "subforced", + "fansub", + "hardsub" + ], + "language_affixes": [ + "dublado", + "dubbed", + "dub" + ], + "language_prefixes": [ + "true" + ], + "language_suffixes": [ + "audio" + ], + "weak_affixes": [ + "v", + "audio", + "true" + ] + }, + "part": { + "prefixes": [ + "pt", + "part" + ] + }, + "release_group": { + "forbidden_names": [ + "rip", + "by", + "for", + "par", + "pour", + "bonus" + ], + "ignored_seps": "[]{}()" + }, + "screen_size": { + "frame_rates": [ + "23.976", + "24", + "25", + "30", + "48", + "50", + "60", + "120" + ], + "min_ar": 1.333, + "max_ar": 1.898, + "interlaced": [ + "360", + "480", + "576", + "900", + "1080" + ], + "progressive": [ + "360", + "480", + "576", + "900", + "1080", + "368", + "720", + "1440", + "2160", + "4320" + ] + }, + "website": { + "safe_tlds": [ + "com", + "org", + "net" + ], + "safe_subdomains": [ + "www" + ], + "safe_prefixes": [ + "co", + "com", + "org", + "net" + ], + "prefixes": [ + "from" + ] + } + } +}
\ No newline at end of file diff --git a/libs/guessit/jsonutils.py b/libs/guessit/jsonutils.py new file mode 100644 index 000000000..a8bb24e6e --- /dev/null +++ b/libs/guessit/jsonutils.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +JSON Utils +""" +import json + +from six import text_type + +try: + from collections import OrderedDict +except ImportError: # pragma: no-cover + from ordereddict import OrderedDict # pylint:disable=import-error + +from rebulk.match import Match + + +class GuessitEncoder(json.JSONEncoder): + """ + JSON Encoder for guessit response + """ + + def default(self, o): # pylint:disable=method-hidden + if isinstance(o, Match): + ret = OrderedDict() + ret['value'] = o.value + if o.raw: + ret['raw'] = o.raw + ret['start'] = o.start + ret['end'] = o.end + return ret + elif hasattr(o, 'name'): # Babelfish languages/countries long name + return text_type(o.name) + else: # pragma: no cover + return text_type(o) diff --git a/libs/guessit/options.py b/libs/guessit/options.py new file mode 100644 index 000000000..e39df3650 --- /dev/null +++ b/libs/guessit/options.py @@ -0,0 +1,262 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Options +""" +import json +import os +import pkgutil +import shlex +from argparse import ArgumentParser + +import six + + +def build_argument_parser(): + """ + Builds the argument parser + :return: the argument parser + :rtype: ArgumentParser + """ + opts = ArgumentParser() + opts.add_argument(dest='filename', help='Filename or release name to guess', nargs='*') + + naming_opts = opts.add_argument_group("Naming") + naming_opts.add_argument('-t', '--type', dest='type', default=None, + help='The suggested file type: movie, episode. If undefined, type will be guessed.') + naming_opts.add_argument('-n', '--name-only', dest='name_only', action='store_true', default=None, + help='Parse files as name only, considering "/" and "\\" like other separators.') + naming_opts.add_argument('-Y', '--date-year-first', action='store_true', dest='date_year_first', default=None, + help='If short date is found, consider the first digits as the year.') + naming_opts.add_argument('-D', '--date-day-first', action='store_true', dest='date_day_first', default=None, + help='If short date is found, consider the second digits as the day.') + naming_opts.add_argument('-L', '--allowed-languages', action='append', dest='allowed_languages', default=None, + help='Allowed language (can be used multiple times)') + naming_opts.add_argument('-C', '--allowed-countries', action='append', dest='allowed_countries', default=None, + help='Allowed country (can be used multiple times)') + naming_opts.add_argument('-E', '--episode-prefer-number', action='store_true', dest='episode_prefer_number', + default=None, + help='Guess "serie.213.avi" as the episode 213. Without this option, ' + 'it will be guessed as season 2, episode 13') + naming_opts.add_argument('-T', '--expected-title', action='append', dest='expected_title', default=None, + help='Expected title to parse (can be used multiple times)') + naming_opts.add_argument('-G', '--expected-group', action='append', dest='expected_group', default=None, + help='Expected release group (can be used multiple times)') + naming_opts.add_argument('--includes', action='append', dest='includes', default=None, + help='List of properties to be detected') + naming_opts.add_argument('--excludes', action='append', dest='excludes', default=None, + help='List of properties to be ignored') + + input_opts = opts.add_argument_group("Input") + input_opts.add_argument('-f', '--input-file', dest='input_file', default=None, + help='Read filenames from an input text file. File should use UTF-8 charset.') + + output_opts = opts.add_argument_group("Output") + output_opts.add_argument('-v', '--verbose', action='store_true', dest='verbose', default=None, + help='Display debug output') + output_opts.add_argument('-P', '--show-property', dest='show_property', default=None, + help='Display the value of a single property (title, series, video_codec, year, ...)') + output_opts.add_argument('-a', '--advanced', dest='advanced', action='store_true', default=None, + help='Display advanced information for filename guesses, as json output') + output_opts.add_argument('-s', '--single-value', dest='single_value', action='store_true', default=None, + help='Keep only first value found for each property') + output_opts.add_argument('-l', '--enforce-list', dest='enforce_list', action='store_true', default=None, + help='Wrap each found value in a list even when property has a single value') + output_opts.add_argument('-j', '--json', dest='json', action='store_true', default=None, + help='Display information for filename guesses as json output') + output_opts.add_argument('-y', '--yaml', dest='yaml', action='store_true', default=None, + help='Display information for filename guesses as yaml output') + + conf_opts = opts.add_argument_group("Configuration") + conf_opts.add_argument('-c', '--config', dest='config', action='append', default=None, + help='Filepath to the configuration file. Configuration contains the same options as ' + 'those command line options, but option names have "-" characters replaced with "_". ' + 'If not defined, guessit tries to read a configuration default configuration file at ' + '~/.guessit/options.(json|yml|yaml) and ~/.config/guessit/options.(json|yml|yaml). ' + 'Set to "false" to disable default configuration file loading.') + conf_opts.add_argument('--no-embedded-config', dest='no_embedded_config', action='store_true', + default=None, + help='Disable default configuration.') + + information_opts = opts.add_argument_group("Information") + information_opts.add_argument('-p', '--properties', dest='properties', action='store_true', default=None, + help='Display properties that can be guessed.') + information_opts.add_argument('-V', '--values', dest='values', action='store_true', default=None, + help='Display property values that can be guessed.') + information_opts.add_argument('--version', dest='version', action='store_true', default=None, + help='Display the guessit version.') + + return opts + + +def parse_options(options=None, api=False): + """ + Parse given option string + + :param options: + :type options: + :param api + :type api: boolean + :return: + :rtype: + """ + if isinstance(options, six.string_types): + args = shlex.split(options) + options = vars(argument_parser.parse_args(args)) + elif options is None: + if api: + options = {} + else: + options = vars(argument_parser.parse_args()) + elif not isinstance(options, dict): + options = vars(argument_parser.parse_args(options)) + return options + + +argument_parser = build_argument_parser() + + +class ConfigurationException(Exception): + """ + Exception related to configuration file. + """ + pass + + +def load_config(options): + """ + Load configuration from configuration file, if defined. + :param options: + :type options: + :return: + :rtype: + """ + config_files_enabled = True + custom_config_files = None + if options.get('config') is not None: + custom_config_files = options.get('config') + if not custom_config_files \ + or not custom_config_files[0] \ + or custom_config_files[0].lower() in ['0', 'no', 'false', 'disabled']: + config_files_enabled = False + + configurations = [] + if config_files_enabled: + home_directory = os.path.expanduser("~") + cwd = os.getcwd() + yaml_supported = False + try: + import yaml # pylint: disable=unused-variable + yaml_supported = True + except ImportError: + pass + config_file_locations = get_config_file_locations(home_directory, cwd, yaml_supported) + config_files = [f for f in config_file_locations if os.path.exists(f)] + + if custom_config_files: + config_files = config_files + custom_config_files + + for config_file in config_files: + config_file_options = load_config_file(config_file) + if config_file_options: + configurations.append(config_file_options) + + embedded_options_data = pkgutil.get_data('guessit', 'config/options.json').decode("utf-8") + embedded_options = json.loads(embedded_options_data) + if not options.get('no_embedded_config'): + configurations.append(embedded_options) + else: + configurations.append({'advanced_config': embedded_options['advanced_config']}) + + if configurations: + configurations.append(options) + return merge_configurations(*configurations) + + return options + + +def merge_configurations(*configurations): + """ + Merge configurations into a single options dict. + :param configurations: + :type configurations: + :return: + :rtype: + """ + + merged = {} + + for options in configurations: + pristine = options.get('pristine') + + if pristine: + if pristine is True: + merged = {} + else: + for to_reset in pristine: + if to_reset in merged: + del merged[to_reset] + + for (option, value) in options.items(): + if value is not None and option != 'pristine': + if option in merged.keys() and isinstance(merged[option], list): + merged[option].extend(value) + elif isinstance(value, list): + merged[option] = list(value) + else: + merged[option] = value + + return merged + + +def load_config_file(filepath): + """ + Load a configuration as an options dict. + + Format of the file is given with filepath extension. + :param filepath: + :type filepath: + :return: + :rtype: + """ + if filepath.endswith('.json'): + with open(filepath) as config_file_data: + return json.load(config_file_data) + if filepath.endswith('.yaml') or filepath.endswith('.yml'): + try: + import yaml + with open(filepath) as config_file_data: + return yaml.load(config_file_data) + except ImportError: # pragma: no cover + raise ConfigurationException('Configuration file extension is not supported. ' + 'PyYAML should be installed to support "%s" file' % ( + filepath,)) + raise ConfigurationException('Configuration file extension is not supported for "%s" file.' % (filepath,)) + + +def get_config_file_locations(homedir, cwd, yaml_supported=False): + """ + Get all possible locations for configuration file. + :param homedir: user home directory + :type homedir: basestring + :param cwd: current working directory + :type homedir: basestring + :return: + :rtype: list + """ + locations = [] + + configdirs = [(os.path.join(homedir, '.guessit'), 'options'), + (os.path.join(homedir, '.config', 'guessit'), 'options'), + (cwd, 'guessit.options')] + configexts = ['json'] + + if yaml_supported: + configexts.append('yaml') + configexts.append('yml') + + for configdir in configdirs: + for configext in configexts: + locations.append(os.path.join(configdir[0], configdir[1] + '.' + configext)) + + return locations diff --git a/libs/guessit/reutils.py b/libs/guessit/reutils.py new file mode 100644 index 000000000..0b654d27c --- /dev/null +++ b/libs/guessit/reutils.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Utils for re module +""" + +from rebulk.remodule import re + + +def build_or_pattern(patterns, name=None, escape=False): + """ + Build a or pattern string from a list of possible patterns + + :param patterns: + :type patterns: + :param name: + :type name: + :param escape: + :type escape: + :return: + :rtype: + """ + or_pattern = [] + for pattern in patterns: + if not or_pattern: + or_pattern.append('(?') + if name: + or_pattern.append('P<' + name + '>') + else: + or_pattern.append(':') + else: + or_pattern.append('|') + or_pattern.append('(?:%s)' % re.escape(pattern) if escape else pattern) + or_pattern.append(')') + return ''.join(or_pattern) diff --git a/libs/guessit/rules/__init__.py b/libs/guessit/rules/__init__.py new file mode 100644 index 000000000..f16bc4e0f --- /dev/null +++ b/libs/guessit/rules/__init__.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Rebulk object default builder +""" +from rebulk import Rebulk + +from .markers.path import path +from .markers.groups import groups + +from .properties.episodes import episodes +from .properties.container import container +from .properties.source import source +from .properties.video_codec import video_codec +from .properties.audio_codec import audio_codec +from .properties.screen_size import screen_size +from .properties.website import website +from .properties.date import date +from .properties.title import title +from .properties.episode_title import episode_title +from .properties.language import language +from .properties.country import country +from .properties.release_group import release_group +from .properties.streaming_service import streaming_service +from .properties.other import other +from .properties.size import size +from .properties.bit_rate import bit_rate +from .properties.edition import edition +from .properties.cds import cds +from .properties.bonus import bonus +from .properties.film import film +from .properties.part import part +from .properties.crc import crc +from .properties.mimetype import mimetype +from .properties.type import type_ + +from .processors import processors + + +def rebulk_builder(config): + """ + Default builder for main Rebulk object used by api. + :return: Main Rebulk object + :rtype: Rebulk + """ + def _config(name): + return config.get(name, {}) + + rebulk = Rebulk() + + common_words = frozenset(_config('common_words')) + + rebulk.rebulk(path(_config('path'))) + rebulk.rebulk(groups(_config('groups'))) + + rebulk.rebulk(episodes(_config('episodes'))) + rebulk.rebulk(container(_config('container'))) + rebulk.rebulk(source(_config('source'))) + rebulk.rebulk(video_codec(_config('video_codec'))) + rebulk.rebulk(audio_codec(_config('audio_codec'))) + rebulk.rebulk(screen_size(_config('screen_size'))) + rebulk.rebulk(website(_config('website'))) + rebulk.rebulk(date(_config('date'))) + rebulk.rebulk(title(_config('title'))) + rebulk.rebulk(episode_title(_config('episode_title'))) + rebulk.rebulk(language(_config('language'), common_words)) + rebulk.rebulk(country(_config('country'), common_words)) + rebulk.rebulk(release_group(_config('release_group'))) + rebulk.rebulk(streaming_service(_config('streaming_service'))) + rebulk.rebulk(other(_config('other'))) + rebulk.rebulk(size(_config('size'))) + rebulk.rebulk(bit_rate(_config('bit_rate'))) + rebulk.rebulk(edition(_config('edition'))) + rebulk.rebulk(cds(_config('cds'))) + rebulk.rebulk(bonus(_config('bonus'))) + rebulk.rebulk(film(_config('film'))) + rebulk.rebulk(part(_config('part'))) + rebulk.rebulk(crc(_config('crc'))) + + rebulk.rebulk(processors(_config('processors'))) + + rebulk.rebulk(mimetype(_config('mimetype'))) + rebulk.rebulk(type_(_config('type'))) + + def customize_properties(properties): + """ + Customize default rebulk properties + """ + count = properties['count'] + del properties['count'] + + properties['season_count'] = count + properties['episode_count'] = count + + return properties + + rebulk.customize_properties = customize_properties + + return rebulk diff --git a/libs/guessit/rules/common/__init__.py b/libs/guessit/rules/common/__init__.py new file mode 100644 index 000000000..444dc72a9 --- /dev/null +++ b/libs/guessit/rules/common/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Common module +""" +import re + +seps = r' [](){}+*|=-_~#/\\.,;:' # list of tags/words separators +seps_no_groups = seps.replace('[](){}', '') +seps_no_fs = seps.replace('/', '').replace('\\', '') + +title_seps = r'-+/\|' # separators for title + +dash = (r'-', r'['+re.escape(seps_no_fs)+']') # abbreviation used by many rebulk objects. +alt_dash = (r'@', r'['+re.escape(seps_no_fs)+']') # abbreviation used by many rebulk objects. diff --git a/libs/guessit/rules/common/comparators.py b/libs/guessit/rules/common/comparators.py new file mode 100644 index 000000000..f46f0c119 --- /dev/null +++ b/libs/guessit/rules/common/comparators.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Comparators +""" +try: + from functools import cmp_to_key +except ImportError: + from ...backports import cmp_to_key + + +def marker_comparator_predicate(match): + """ + Match predicate used in comparator + """ + return ( + not match.private + and match.name not in ('proper_count', 'title') + and not (match.name == 'container' and 'extension' in match.tags) + and not (match.name == 'other' and match.value == 'Rip') + ) + + +def marker_weight(matches, marker, predicate): + """ + Compute the comparator weight of a marker + :param matches: + :param marker: + :param predicate: + :return: + """ + return len(set(match.name for match in matches.range(*marker.span, predicate=predicate))) + + +def marker_comparator(matches, markers, predicate): + """ + Builds a comparator that returns markers sorted from the most valuable to the less. + + Take the parts where matches count is higher, then when length is higher, then when position is at left. + + :param matches: + :type matches: + :param markers: + :param predicate: + :return: + :rtype: + """ + + def comparator(marker1, marker2): + """ + The actual comparator function. + """ + matches_count = marker_weight(matches, marker2, predicate) - marker_weight(matches, marker1, predicate) + if matches_count: + return matches_count + + # give preference to rightmost path + return markers.index(marker2) - markers.index(marker1) + + return comparator + + +def marker_sorted(markers, matches, predicate=marker_comparator_predicate): + """ + Sort markers from matches, from the most valuable to the less. + + :param markers: + :type markers: + :param matches: + :type matches: + :param predicate: + :return: + :rtype: + """ + return sorted(markers, key=cmp_to_key(marker_comparator(matches, markers, predicate=predicate))) diff --git a/libs/guessit/rules/common/date.py b/libs/guessit/rules/common/date.py new file mode 100644 index 000000000..cef31fdcd --- /dev/null +++ b/libs/guessit/rules/common/date.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Date +""" +from dateutil import parser + +from rebulk.remodule import re + +_dsep = r'[-/ \.]' +_dsep_bis = r'[-/ \.x]' + +date_regexps = [ + re.compile(r'%s((\d{8}))%s' % (_dsep, _dsep), re.IGNORECASE), + re.compile(r'%s((\d{6}))%s' % (_dsep, _dsep), re.IGNORECASE), + re.compile(r'(?:^|[^\d])((\d{2})%s(\d{1,2})%s(\d{1,2}))(?:$|[^\d])' % (_dsep, _dsep), re.IGNORECASE), + re.compile(r'(?:^|[^\d])((\d{1,2})%s(\d{1,2})%s(\d{2}))(?:$|[^\d])' % (_dsep, _dsep), re.IGNORECASE), + re.compile(r'(?:^|[^\d])((\d{4})%s(\d{1,2})%s(\d{1,2}))(?:$|[^\d])' % (_dsep_bis, _dsep), re.IGNORECASE), + re.compile(r'(?:^|[^\d])((\d{1,2})%s(\d{1,2})%s(\d{4}))(?:$|[^\d])' % (_dsep, _dsep_bis), re.IGNORECASE), + re.compile(r'(?:^|[^\d])((\d{1,2}(?:st|nd|rd|th)?%s(?:[a-z]{3,10})%s\d{4}))(?:$|[^\d])' % (_dsep, _dsep), + re.IGNORECASE)] + + +def valid_year(year): + """Check if number is a valid year""" + return 1920 <= year < 2030 + + +def _is_int(string): + """ + Check if the input string is an integer + + :param string: + :type string: + :return: + :rtype: + """ + try: + int(string) + return True + except ValueError: + return False + + +def _guess_day_first_parameter(groups): # pylint:disable=inconsistent-return-statements + """ + If day_first is not defined, use some heuristic to fix it. + It helps to solve issues with python dateutils 2.5.3 parser changes. + + :param groups: match groups found for the date + :type groups: list of match objects + :return: day_first option guessed value + :rtype: bool + """ + + # If match starts with a long year, then day_first is force to false. + if _is_int(groups[0]) and valid_year(int(groups[0][:4])): + return False + # If match ends with a long year, the day_first is forced to true. + elif _is_int(groups[-1]) and valid_year(int(groups[-1][-4:])): + return True + # If match starts with a short year, then day_first is force to false. + elif _is_int(groups[0]) and int(groups[0][:2]) > 31: + return False + # If match ends with a short year, then day_first is force to true. + elif _is_int(groups[-1]) and int(groups[-1][-2:]) > 31: + return True + + +def search_date(string, year_first=None, day_first=None): # pylint:disable=inconsistent-return-statements + """Looks for date patterns, and if found return the date and group span. + + Assumes there are sentinels at the beginning and end of the string that + always allow matching a non-digit delimiting the date. + + Year can be defined on two digit only. It will return the nearest possible + date from today. + + >>> search_date(' This happened on 2002-04-22. ') + (18, 28, datetime.date(2002, 4, 22)) + + >>> search_date(' And this on 17-06-1998. ') + (13, 23, datetime.date(1998, 6, 17)) + + >>> search_date(' no date in here ') + """ + for date_re in date_regexps: + search_match = date_re.search(string) + if not search_match: + continue + + start, end = search_match.start(1), search_match.end(1) + groups = search_match.groups()[1:] + match = '-'.join(groups) + + if match is None: + continue + + if year_first and day_first is None: + day_first = False + + if day_first is None: + day_first = _guess_day_first_parameter(groups) + + # If day_first/year_first is undefined, parse is made using both possible values. + yearfirst_opts = [False, True] + if year_first is not None: + yearfirst_opts = [year_first] + + dayfirst_opts = [True, False] + if day_first is not None: + dayfirst_opts = [day_first] + + kwargs_list = ({'dayfirst': d, 'yearfirst': y} + for d in dayfirst_opts for y in yearfirst_opts) + for kwargs in kwargs_list: + try: + date = parser.parse(match, **kwargs) + except (ValueError, TypeError): # pragma: no cover + # see https://bugs.launchpad.net/dateutil/+bug/1247643 + date = None + + # check date plausibility + if date and valid_year(date.year): # pylint:disable=no-member + return start, end, date.date() # pylint:disable=no-member diff --git a/libs/guessit/rules/common/expected.py b/libs/guessit/rules/common/expected.py new file mode 100644 index 000000000..eae562a2d --- /dev/null +++ b/libs/guessit/rules/common/expected.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Expected property factory +""" +import re + +from rebulk import Rebulk +from rebulk.utils import find_all + +from . import dash, seps + + +def build_expected_function(context_key): + """ + Creates a expected property function + :param context_key: + :type context_key: + :param cleanup: + :type cleanup: + :return: + :rtype: + """ + + def expected(input_string, context): + """ + Expected property functional pattern. + :param input_string: + :type input_string: + :param context: + :type context: + :return: + :rtype: + """ + ret = [] + for search in context.get(context_key): + if search.startswith('re:'): + search = search[3:] + search = search.replace(' ', '-') + matches = Rebulk().regex(search, abbreviations=[dash], flags=re.IGNORECASE) \ + .matches(input_string, context) + for match in matches: + ret.append(match.span) + else: + value = search + for sep in seps: + input_string = input_string.replace(sep, ' ') + search = search.replace(sep, ' ') + for start in find_all(input_string, search, ignore_case=True): + ret.append({'start': start, 'end': start + len(search), 'value': value}) + return ret + + return expected diff --git a/libs/guessit/rules/common/formatters.py b/libs/guessit/rules/common/formatters.py new file mode 100644 index 000000000..6bd09b159 --- /dev/null +++ b/libs/guessit/rules/common/formatters.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Formatters +""" +from rebulk.formatters import formatters +from rebulk.remodule import re +from . import seps + +_excluded_clean_chars = ',:;-/\\' +clean_chars = "" +for sep in seps: + if sep not in _excluded_clean_chars: + clean_chars += sep + + +def _potential_before(i, input_string): + """ + Check if the character at position i can be a potential single char separator considering what's before it. + + :param i: + :type i: int + :param input_string: + :type input_string: str + :return: + :rtype: bool + """ + return i - 2 >= 0 and input_string[i] == input_string[i - 2] and input_string[i - 1] not in seps + + +def _potential_after(i, input_string): + """ + Check if the character at position i can be a potential single char separator considering what's after it. + + :param i: + :type i: int + :param input_string: + :type input_string: str + :return: + :rtype: bool + """ + return i + 2 >= len(input_string) or \ + input_string[i + 2] == input_string[i] and input_string[i + 1] not in seps + + +def cleanup(input_string): + """ + Removes and strip separators from input_string (but keep ',;' characters) + + It also keep separators for single characters (Mavels Agents of S.H.I.E.L.D.) + + :param input_string: + :type input_string: str + :return: + :rtype: + """ + clean_string = input_string + for char in clean_chars: + clean_string = clean_string.replace(char, ' ') + + # Restore input separator if they separate single characters. + # Useful for Mavels Agents of S.H.I.E.L.D. + # https://github.com/guessit-io/guessit/issues/278 + + indices = [i for i, letter in enumerate(clean_string) if letter in seps] + + dots = set() + if indices: + clean_list = list(clean_string) + + potential_indices = [] + + for i in indices: + if _potential_before(i, input_string) and _potential_after(i, input_string): + potential_indices.append(i) + + replace_indices = [] + + for potential_index in potential_indices: + if potential_index - 2 in potential_indices or potential_index + 2 in potential_indices: + replace_indices.append(potential_index) + + if replace_indices: + for replace_index in replace_indices: + dots.add(input_string[replace_index]) + clean_list[replace_index] = input_string[replace_index] + clean_string = ''.join(clean_list) + + clean_string = strip(clean_string, ''.join([c for c in seps if c not in dots])) + + clean_string = re.sub(' +', ' ', clean_string) + return clean_string + + +def strip(input_string, chars=seps): + """ + Strip separators from input_string + :param input_string: + :param chars: + :type input_string: + :return: + :rtype: + """ + return input_string.strip(chars) + + +def raw_cleanup(raw): + """ + Cleanup a raw value to perform raw comparison + :param raw: + :type raw: + :return: + :rtype: + """ + return formatters(cleanup, strip)(raw.lower()) + + +def reorder_title(title, articles=('the',), separators=(',', ', ')): + """ + Reorder the title + :param title: + :type title: + :param articles: + :type articles: + :param separators: + :type separators: + :return: + :rtype: + """ + ltitle = title.lower() + for article in articles: + for separator in separators: + suffix = separator + article + if ltitle[-len(suffix):] == suffix: + return title[-len(suffix) + len(separator):] + ' ' + title[:-len(suffix)] + return title diff --git a/libs/guessit/rules/common/numeral.py b/libs/guessit/rules/common/numeral.py new file mode 100644 index 000000000..7c064fdb6 --- /dev/null +++ b/libs/guessit/rules/common/numeral.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +parse numeral from various formats +""" +from rebulk.remodule import re + +digital_numeral = r'\d{1,4}' + +roman_numeral = r'(?=[MCDLXVI]+)M{0,4}(?:CM|CD|D?C{0,3})(?:XC|XL|L?X{0,3})(?:IX|IV|V?I{0,3})' + +english_word_numeral_list = [ + 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten', + 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen', 'twenty' +] + +french_word_numeral_list = [ + 'zéro', 'un', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', 'dix', + 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dix-sept', 'dix-huit', 'dix-neuf', 'vingt' +] + +french_alt_word_numeral_list = [ + 'zero', 'une', 'deux', 'trois', 'quatre', 'cinq', 'six', 'sept', 'huit', 'neuf', 'dix', + 'onze', 'douze', 'treize', 'quatorze', 'quinze', 'seize', 'dixsept', 'dixhuit', 'dixneuf', 'vingt' +] + + +def __build_word_numeral(*args): + """ + Build word numeral regexp from list. + + :param args: + :type args: + :param kwargs: + :type kwargs: + :return: + :rtype: + """ + re_ = None + for word_list in args: + for word in word_list: + if not re_: + re_ = r'(?:(?=\w+)' + else: + re_ += '|' + re_ += word + re_ += ')' + return re_ + + +word_numeral = __build_word_numeral(english_word_numeral_list, french_word_numeral_list, french_alt_word_numeral_list) + +numeral = '(?:' + digital_numeral + '|' + roman_numeral + '|' + word_numeral + ')' + +__romanNumeralMap = ( + ('M', 1000), + ('CM', 900), + ('D', 500), + ('CD', 400), + ('C', 100), + ('XC', 90), + ('L', 50), + ('XL', 40), + ('X', 10), + ('IX', 9), + ('V', 5), + ('IV', 4), + ('I', 1) +) + +__romanNumeralPattern = re.compile('^' + roman_numeral + '$') + + +def __parse_roman(value): + """ + convert Roman numeral to integer + + :param value: Value to parse + :type value: string + :return: + :rtype: + """ + if not __romanNumeralPattern.search(value): + raise ValueError('Invalid Roman numeral: %s' % value) + + result = 0 + index = 0 + for num, integer in __romanNumeralMap: + while value[index:index + len(num)] == num: + result += integer + index += len(num) + return result + + +def __parse_word(value): + """ + Convert Word numeral to integer + + :param value: Value to parse + :type value: string + :return: + :rtype: + """ + for word_list in [english_word_numeral_list, french_word_numeral_list, french_alt_word_numeral_list]: + try: + return word_list.index(value.lower()) + except ValueError: + pass + raise ValueError # pragma: no cover + + +_clean_re = re.compile(r'[^\d]*(\d+)[^\d]*') + + +def parse_numeral(value, int_enabled=True, roman_enabled=True, word_enabled=True, clean=True): + """ + Parse a numeric value into integer. + + :param value: Value to parse. Can be an integer, roman numeral or word. + :type value: string + :param int_enabled: + :type int_enabled: + :param roman_enabled: + :type roman_enabled: + :param word_enabled: + :type word_enabled: + :param clean: + :type clean: + :return: Numeric value, or None if value can't be parsed + :rtype: int + """ + # pylint: disable=too-many-branches + if int_enabled: + try: + if clean: + match = _clean_re.match(value) + if match: + clean_value = match.group(1) + return int(clean_value) + return int(value) + except ValueError: + pass + if roman_enabled: + try: + if clean: + for word in value.split(): + try: + return __parse_roman(word.upper()) + except ValueError: + pass + return __parse_roman(value) + except ValueError: + pass + if word_enabled: + try: + if clean: + for word in value.split(): + try: + return __parse_word(word) + except ValueError: # pragma: no cover + pass + return __parse_word(value) # pragma: no cover + except ValueError: # pragma: no cover + pass + raise ValueError('Invalid numeral: ' + value) # pragma: no cover diff --git a/libs/guessit/rules/common/pattern.py b/libs/guessit/rules/common/pattern.py new file mode 100644 index 000000000..5f560f2c9 --- /dev/null +++ b/libs/guessit/rules/common/pattern.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Pattern utility functions +""" + + +def is_disabled(context, name): + """Whether a specific pattern is disabled. + + The context object might define an inclusion list (includes) or an exclusion list (excludes) + A pattern is considered disabled if it's found in the exclusion list or + it's not found in the inclusion list and the inclusion list is not empty or not defined. + + :param context: + :param name: + :return: + """ + if not context: + return False + + excludes = context.get('excludes') + if excludes and name in excludes: + return True + + includes = context.get('includes') + return includes and name not in includes diff --git a/libs/guessit/rules/common/quantity.py b/libs/guessit/rules/common/quantity.py new file mode 100644 index 000000000..bbd41fbb9 --- /dev/null +++ b/libs/guessit/rules/common/quantity.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Quantities: Size +""" +import re +from abc import abstractmethod + +import six + +from ..common import seps + + +class Quantity(object): + """ + Represent a quantity object with magnitude and units. + """ + + parser_re = re.compile(r'(?P<magnitude>\d+(?:[.]\d+)?)(?P<units>[^\d]+)?') + + def __init__(self, magnitude, units): + self.magnitude = magnitude + self.units = units + + @classmethod + @abstractmethod + def parse_units(cls, value): + """ + Parse a string to a proper unit notation. + """ + raise NotImplementedError + + @classmethod + def fromstring(cls, string): + """ + Parse the string into a quantity object. + :param string: + :return: + """ + values = cls.parser_re.match(string).groupdict() + try: + magnitude = int(values['magnitude']) + except ValueError: + magnitude = float(values['magnitude']) + units = cls.parse_units(values['units']) + + return cls(magnitude, units) + + def __hash__(self): + return hash(str(self)) + + def __eq__(self, other): + if isinstance(other, six.string_types): + return str(self) == other + if not isinstance(other, self.__class__): + return NotImplemented + return self.magnitude == other.magnitude and self.units == other.units + + def __ne__(self, other): + return not self == other + + def __repr__(self): + return '<{0} [{1}]>'.format(self.__class__.__name__, self) + + def __str__(self): + return '{0}{1}'.format(self.magnitude, self.units) + + +class Size(Quantity): + """ + Represent size. + + e.g.: 1.1GB, 300MB + """ + + @classmethod + def parse_units(cls, value): + return value.strip(seps).upper() + + +class BitRate(Quantity): + """ + Represent bit rate. + + e.g.: 320Kbps, 1.5Mbps + """ + + @classmethod + def parse_units(cls, value): + value = value.strip(seps).capitalize() + for token in ('bits', 'bit'): + value = value.replace(token, 'bps') + + return value + + +class FrameRate(Quantity): + """ + Represent frame rate. + + e.g.: 24fps, 60fps + """ + + @classmethod + def parse_units(cls, value): + return 'fps' diff --git a/libs/guessit/rules/common/validators.py b/libs/guessit/rules/common/validators.py new file mode 100644 index 000000000..0e79b9896 --- /dev/null +++ b/libs/guessit/rules/common/validators.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Validators +""" +from functools import partial + +from rebulk.validators import chars_before, chars_after, chars_surround +from . import seps + +seps_before = partial(chars_before, seps) +seps_after = partial(chars_after, seps) +seps_surround = partial(chars_surround, seps) + + +def int_coercable(string): + """ + Check if string can be coerced to int + :param string: + :type string: + :return: + :rtype: + """ + try: + int(string) + return True + except ValueError: + return False + + +def compose(*validators): + """ + Compose validators functions + :param validators: + :type validators: + :return: + :rtype: + """ + def composed(string): + """ + Composed validators function + :param string: + :type string: + :return: + :rtype: + """ + for validator in validators: + if not validator(string): + return False + return True + return composed diff --git a/libs/guessit/rules/common/words.py b/libs/guessit/rules/common/words.py new file mode 100644 index 000000000..cccbc7d23 --- /dev/null +++ b/libs/guessit/rules/common/words.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Words utils +""" +from collections import namedtuple + +from . import seps + +_Word = namedtuple('_Word', ['span', 'value']) + + +def iter_words(string): + """ + Iterate on all words in a string + :param string: + :type string: + :return: + :rtype: iterable[str] + """ + i = 0 + last_sep_index = -1 + inside_word = False + for char in string: + if ord(char) < 128 and char in seps: # Make sure we don't exclude unicode characters. + if inside_word: + yield _Word(span=(last_sep_index+1, i), value=string[last_sep_index+1:i]) + inside_word = False + last_sep_index = i + else: + inside_word = True + i += 1 + if inside_word: + yield _Word(span=(last_sep_index+1, i), value=string[last_sep_index+1:i]) diff --git a/libs/guessit/rules/markers/__init__.py b/libs/guessit/rules/markers/__init__.py new file mode 100644 index 000000000..6a48a13b3 --- /dev/null +++ b/libs/guessit/rules/markers/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Markers +""" diff --git a/libs/guessit/rules/markers/groups.py b/libs/guessit/rules/markers/groups.py new file mode 100644 index 000000000..4716d15d7 --- /dev/null +++ b/libs/guessit/rules/markers/groups.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Groups markers (...), [...] and {...} +""" +from rebulk import Rebulk + + +def groups(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk() + rebulk.defaults(name="group", marker=True) + + starting = config['starting'] + ending = config['ending'] + + def mark_groups(input_string): + """ + Functional pattern to mark groups (...), [...] and {...}. + + :param input_string: + :return: + """ + openings = ([], [], []) + i = 0 + + ret = [] + for char in input_string: + start_type = starting.find(char) + if start_type > -1: + openings[start_type].append(i) + + i += 1 + + end_type = ending.find(char) + if end_type > -1: + try: + start_index = openings[end_type].pop() + ret.append((start_index, i)) + except IndexError: + pass + return ret + + rebulk.functional(mark_groups) + return rebulk diff --git a/libs/guessit/rules/markers/path.py b/libs/guessit/rules/markers/path.py new file mode 100644 index 000000000..6d993b75a --- /dev/null +++ b/libs/guessit/rules/markers/path.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Path markers +""" +from rebulk import Rebulk + +from rebulk.utils import find_all + + +def path(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk() + rebulk.defaults(name="path", marker=True) + + def mark_path(input_string, context): + """ + Functional pattern to mark path elements. + + :param input_string: + :param context: + :return: + """ + ret = [] + if context.get('name_only', False): + ret.append((0, len(input_string))) + else: + indices = list(find_all(input_string, '/')) + indices += list(find_all(input_string, '\\')) + indices += [-1, len(input_string)] + + indices.sort() + + for i in range(0, len(indices) - 1): + ret.append((indices[i] + 1, indices[i + 1])) + + return ret + + rebulk.functional(mark_path) + return rebulk diff --git a/libs/guessit/rules/processors.py b/libs/guessit/rules/processors.py new file mode 100644 index 000000000..6f5d731f6 --- /dev/null +++ b/libs/guessit/rules/processors.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Processors +""" +from collections import defaultdict +import copy + +import six + +from rebulk import Rebulk, Rule, CustomRule, POST_PROCESS, PRE_PROCESS, AppendMatch, RemoveMatch + +from .common import seps_no_groups +from .common.formatters import cleanup +from .common.comparators import marker_sorted +from .common.date import valid_year +from .common.words import iter_words + + +class EnlargeGroupMatches(CustomRule): + """ + Enlarge matches that are starting and/or ending group to include brackets in their span. + """ + priority = PRE_PROCESS + + def when(self, matches, context): + starting = [] + ending = [] + + for group in matches.markers.named('group'): + for match in matches.starting(group.start + 1): + starting.append(match) + + for match in matches.ending(group.end - 1): + ending.append(match) + + return starting, ending + + def then(self, matches, when_response, context): + starting, ending = when_response + for match in starting: + matches.remove(match) + match.start -= 1 + match.raw_start += 1 + matches.append(match) + + for match in ending: + matches.remove(match) + match.end += 1 + match.raw_end -= 1 + matches.append(match) + + +class EquivalentHoles(Rule): + """ + Creates equivalent matches for holes that have same values than existing (case insensitive) + """ + priority = POST_PROCESS + consequence = AppendMatch + + def when(self, matches, context): + new_matches = [] + + for filepath in marker_sorted(matches.markers.named('path'), matches): + holes = matches.holes(start=filepath.start, end=filepath.end, formatter=cleanup) + for name in matches.names: + for hole in list(holes): + for current_match in matches.named(name): + if isinstance(current_match.value, six.string_types) and \ + hole.value.lower() == current_match.value.lower(): + if 'equivalent-ignore' in current_match.tags: + continue + new_value = _preferred_string(hole.value, current_match.value) + if hole.value != new_value: + hole.value = new_value + if current_match.value != new_value: + current_match.value = new_value + hole.name = name + hole.tags = ['equivalent'] + new_matches.append(hole) + if hole in holes: + holes.remove(hole) + + return new_matches + + +class RemoveAmbiguous(Rule): + """ + If multiple matches are found with same name and different values, keep the one in the most valuable filepart. + Also keep others match with same name and values than those kept ones. + """ + + priority = POST_PROCESS + consequence = RemoveMatch + + def __init__(self, sort_function=marker_sorted, predicate=None): + super(RemoveAmbiguous, self).__init__() + self.sort_function = sort_function + self.predicate = predicate + + def when(self, matches, context): + fileparts = self.sort_function(matches.markers.named('path'), matches) + + previous_fileparts_names = set() + values = defaultdict(list) + + to_remove = [] + for filepart in fileparts: + filepart_matches = matches.range(filepart.start, filepart.end, predicate=self.predicate) + + filepart_names = set() + for match in filepart_matches: + filepart_names.add(match.name) + if match.name in previous_fileparts_names: + if match.value not in values[match.name]: + to_remove.append(match) + else: + if match.value not in values[match.name]: + values[match.name].append(match.value) + + previous_fileparts_names.update(filepart_names) + + return to_remove + + +class RemoveLessSpecificSeasonEpisode(RemoveAmbiguous): + """ + If multiple season/episodes matches are found with different values, + keep the one tagged as 'SxxExx' or in the rightmost filepart. + """ + def __init__(self, name): + super(RemoveLessSpecificSeasonEpisode, self).__init__( + sort_function=(lambda markers, matches: + marker_sorted(list(reversed(markers)), matches, + lambda match: match.name == name and 'SxxExx' in match.tags)), + predicate=lambda match: match.name == name) + + +def _preferred_string(value1, value2): # pylint:disable=too-many-return-statements + """ + Retrieves preferred title from both values. + :param value1: + :type value1: str + :param value2: + :type value2: str + :return: The preferred title + :rtype: str + """ + if value1 == value2: + return value1 + if value1.istitle() and not value2.istitle(): + return value1 + if not value1.isupper() and value2.isupper(): + return value1 + if not value1.isupper() and value1[0].isupper() and not value2[0].isupper(): + return value1 + if _count_title_words(value1) > _count_title_words(value2): + return value1 + return value2 + + +def _count_title_words(value): + """ + Count only many words are titles in value. + :param value: + :type value: + :return: + :rtype: + """ + ret = 0 + for word in iter_words(value): + if word.value.istitle(): + ret += 1 + return ret + + +class SeasonYear(Rule): + """ + If a season is a valid year and no year was found, create an match with year. + """ + priority = POST_PROCESS + consequence = AppendMatch + + def when(self, matches, context): + ret = [] + if not matches.named('year'): + for season in matches.named('season'): + if valid_year(season.value): + year = copy.copy(season) + year.name = 'year' + ret.append(year) + return ret + + +class Processors(CustomRule): + """ + Empty rule for ordering post_processing properly. + """ + priority = POST_PROCESS + + def when(self, matches, context): + pass + + def then(self, matches, when_response, context): # pragma: no cover + pass + + +class StripSeparators(CustomRule): + """ + Strip separators from matches. Keep separators if they are from acronyms, like in ".S.H.I.E.L.D." + """ + priority = POST_PROCESS + + def when(self, matches, context): + return matches + + def then(self, matches, when_response, context): # pragma: no cover + for match in matches: + for _ in range(0, len(match.span)): + if match.raw[0] in seps_no_groups and (len(match.raw) < 3 or match.raw[2] not in seps_no_groups): + match.raw_start += 1 + + for _ in reversed(range(0, len(match.span))): + if match.raw[-1] in seps_no_groups and (len(match.raw) < 3 or match.raw[-3] not in seps_no_groups): + match.raw_end -= 1 + + +def processors(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + return Rebulk().rules(EnlargeGroupMatches, EquivalentHoles, + RemoveLessSpecificSeasonEpisode('season'), + RemoveLessSpecificSeasonEpisode('episode'), + RemoveAmbiguous, SeasonYear, Processors, StripSeparators) diff --git a/libs/guessit/rules/properties/__init__.py b/libs/guessit/rules/properties/__init__.py new file mode 100644 index 000000000..e0a24eaf0 --- /dev/null +++ b/libs/guessit/rules/properties/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Properties +""" diff --git a/libs/guessit/rules/properties/audio_codec.py b/libs/guessit/rules/properties/audio_codec.py new file mode 100644 index 000000000..a1cf585e0 --- /dev/null +++ b/libs/guessit/rules/properties/audio_codec.py @@ -0,0 +1,209 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +audio_codec, audio_profile and audio_channels property +""" +from rebulk.remodule import re + +from rebulk import Rebulk, Rule, RemoveMatch + +from ..common import dash +from ..common.pattern import is_disabled +from ..common.validators import seps_before, seps_after + +audio_properties = ['audio_codec', 'audio_profile', 'audio_channels'] + + +def audio_codec(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]).string_defaults(ignore_case=True) + + def audio_codec_priority(match1, match2): + """ + Gives priority to audio_codec + :param match1: + :type match1: + :param match2: + :type match2: + :return: + :rtype: + """ + if match1.name == 'audio_codec' and match2.name in ['audio_profile', 'audio_channels']: + return match2 + if match1.name in ['audio_profile', 'audio_channels'] and match2.name == 'audio_codec': + return match1 + return '__default__' + + rebulk.defaults(name='audio_codec', + conflict_solver=audio_codec_priority, + disabled=lambda context: is_disabled(context, 'audio_codec')) + + rebulk.regex("MP3", "LAME", r"LAME(?:\d)+-?(?:\d)+", value="MP3") + rebulk.regex('Dolby', 'DolbyDigital', 'Dolby-Digital', 'DD', 'AC3D?', value='Dolby Digital') + rebulk.regex('Dolby-?Atmos', 'Atmos', value='Dolby Atmos') + rebulk.string("AAC", value="AAC") + rebulk.string('EAC3', 'DDP', 'DD+', value='Dolby Digital Plus') + rebulk.string("Flac", value="FLAC") + rebulk.string("DTS", value="DTS") + rebulk.regex('DTS-?HD', 'DTS(?=-?MA)', value='DTS-HD', + conflict_solver=lambda match, other: other if other.name == 'audio_codec' else '__default__') + rebulk.regex('True-?HD', value='Dolby TrueHD') + rebulk.string('Opus', value='Opus') + rebulk.string('Vorbis', value='Vorbis') + rebulk.string('PCM', value='PCM') + rebulk.string('LPCM', value='LPCM') + + rebulk.defaults(name='audio_profile', disabled=lambda context: is_disabled(context, 'audio_profile')) + rebulk.string('MA', value='Master Audio', tags='DTS-HD') + rebulk.string('HR', 'HRA', value='High Resolution Audio', tags='DTS-HD') + rebulk.string('ES', value='Extended Surround', tags='DTS') + rebulk.string('HE', value='High Efficiency', tags='AAC') + rebulk.string('LC', value='Low Complexity', tags='AAC') + rebulk.string('HQ', value='High Quality', tags='Dolby Digital') + rebulk.string('EX', value='EX', tags='Dolby Digital') + + rebulk.defaults(name="audio_channels", disabled=lambda context: is_disabled(context, 'audio_channels')) + rebulk.regex(r'(7[\W_][01](?:ch)?)(?=[^\d]|$)', value='7.1', children=True) + rebulk.regex(r'(5[\W_][01](?:ch)?)(?=[^\d]|$)', value='5.1', children=True) + rebulk.regex(r'(2[\W_]0(?:ch)?)(?=[^\d]|$)', value='2.0', children=True) + rebulk.regex('7[01]', value='7.1', validator=seps_after, tags='weak-audio_channels') + rebulk.regex('5[01]', value='5.1', validator=seps_after, tags='weak-audio_channels') + rebulk.string('20', value='2.0', validator=seps_after, tags='weak-audio_channels') + rebulk.string('7ch', '8ch', value='7.1') + rebulk.string('5ch', '6ch', value='5.1') + rebulk.string('2ch', 'stereo', value='2.0') + rebulk.string('1ch', 'mono', value='1.0') + + rebulk.rules(DtsHDRule, AacRule, DolbyDigitalRule, AudioValidatorRule, HqConflictRule, AudioChannelsValidatorRule) + + return rebulk + + +class AudioValidatorRule(Rule): + """ + Remove audio properties if not surrounded by separators and not next each others + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + ret = [] + + audio_list = matches.range(predicate=lambda match: match.name in audio_properties) + for audio in audio_list: + if not seps_before(audio): + valid_before = matches.range(audio.start - 1, audio.start, + lambda match: match.name in audio_properties) + if not valid_before: + ret.append(audio) + continue + if not seps_after(audio): + valid_after = matches.range(audio.end, audio.end + 1, + lambda match: match.name in audio_properties) + if not valid_after: + ret.append(audio) + continue + + return ret + + +class AudioProfileRule(Rule): + """ + Abstract rule to validate audio profiles + """ + priority = 64 + dependency = AudioValidatorRule + consequence = RemoveMatch + + def __init__(self, codec): + super(AudioProfileRule, self).__init__() + self.codec = codec + + def enabled(self, context): + return not is_disabled(context, 'audio_profile') + + def when(self, matches, context): + profile_list = matches.named('audio_profile', lambda match: self.codec in match.tags) + ret = [] + for profile in profile_list: + codec = matches.previous(profile, lambda match: match.name == 'audio_codec' and match.value == self.codec) + if not codec: + codec = matches.next(profile, lambda match: match.name == 'audio_codec' and match.value == self.codec) + if not codec: + ret.append(profile) + if codec: + ret.extend(matches.conflicting(profile)) + return ret + + +class DtsHDRule(AudioProfileRule): + """ + Rule to validate DTS-HD profile + """ + + def __init__(self): + super(DtsHDRule, self).__init__('DTS-HD') + + +class AacRule(AudioProfileRule): + """ + Rule to validate AAC profile + """ + + def __init__(self): + super(AacRule, self).__init__("AAC") + + +class DolbyDigitalRule(AudioProfileRule): + """ + Rule to validate Dolby Digital profile + """ + + def __init__(self): + super(DolbyDigitalRule, self).__init__('Dolby Digital') + + +class HqConflictRule(Rule): + """ + Solve conflict between HQ from other property and from audio_profile. + """ + + dependency = [DtsHDRule, AacRule, DolbyDigitalRule] + consequence = RemoveMatch + + def enabled(self, context): + return not is_disabled(context, 'audio_profile') + + def when(self, matches, context): + hq_audio = matches.named('audio_profile', lambda m: m.value == 'High Quality') + hq_audio_spans = [match.span for match in hq_audio] + return matches.named('other', lambda m: m.span in hq_audio_spans) + + +class AudioChannelsValidatorRule(Rule): + """ + Remove audio_channel if no audio codec as previous match. + """ + priority = 128 + consequence = RemoveMatch + + def enabled(self, context): + return not is_disabled(context, 'audio_channels') + + def when(self, matches, context): + ret = [] + + for audio_channel in matches.tagged('weak-audio_channels'): + valid_before = matches.range(audio_channel.start - 1, audio_channel.start, + lambda match: match.name == 'audio_codec') + if not valid_before: + ret.append(audio_channel) + + return ret diff --git a/libs/guessit/rules/properties/bit_rate.py b/libs/guessit/rules/properties/bit_rate.py new file mode 100644 index 000000000..391f1d2fc --- /dev/null +++ b/libs/guessit/rules/properties/bit_rate.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +video_bit_rate and audio_bit_rate properties +""" +import re + +from rebulk import Rebulk +from rebulk.rules import Rule, RemoveMatch, RenameMatch + +from ..common import dash, seps +from ..common.pattern import is_disabled +from ..common.quantity import BitRate +from ..common.validators import seps_surround + + +def bit_rate(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: (is_disabled(context, 'audio_bit_rate') + and is_disabled(context, 'video_bit_rate'))) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]) + rebulk.defaults(name='audio_bit_rate', validator=seps_surround) + rebulk.regex(r'\d+-?[kmg]b(ps|its?)', r'\d+\.\d+-?[kmg]b(ps|its?)', + conflict_solver=( + lambda match, other: match + if other.name == 'audio_channels' and 'weak-audio_channels' not in other.tags + else other + ), + formatter=BitRate.fromstring, tags=['release-group-prefix']) + + rebulk.rules(BitRateTypeRule) + + return rebulk + + +class BitRateTypeRule(Rule): + """ + Convert audio bit rate guess into video bit rate. + """ + consequence = [RenameMatch('video_bit_rate'), RemoveMatch] + + def when(self, matches, context): + to_rename = [] + to_remove = [] + + if is_disabled(context, 'audio_bit_rate'): + to_remove.extend(matches.named('audio_bit_rate')) + else: + video_bit_rate_disabled = is_disabled(context, 'video_bit_rate') + for match in matches.named('audio_bit_rate'): + previous = matches.previous(match, index=0, + predicate=lambda m: m.name in ('source', 'screen_size', 'video_codec')) + if previous and not matches.holes(previous.end, match.start, predicate=lambda m: m.value.strip(seps)): + after = matches.next(match, index=0, predicate=lambda m: m.name == 'audio_codec') + if after and not matches.holes(match.end, after.start, predicate=lambda m: m.value.strip(seps)): + bitrate = match.value + if bitrate.units == 'Kbps' or (bitrate.units == 'Mbps' and bitrate.magnitude < 10): + continue + + if video_bit_rate_disabled: + to_remove.append(match) + else: + to_rename.append(match) + + return to_rename, to_remove diff --git a/libs/guessit/rules/properties/bonus.py b/libs/guessit/rules/properties/bonus.py new file mode 100644 index 000000000..c4554cd06 --- /dev/null +++ b/libs/guessit/rules/properties/bonus.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +bonus property +""" +from rebulk.remodule import re + +from rebulk import Rebulk, AppendMatch, Rule + +from .title import TitleFromPosition +from ..common.formatters import cleanup +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def bonus(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'bonus')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE) + + rebulk.regex(r'x(\d+)', name='bonus', private_parent=True, children=True, formatter=int, + validator={'__parent__': lambda match: seps_surround}, + conflict_solver=lambda match, conflicting: match + if conflicting.name in ('video_codec', 'episode') and 'weak-episode' not in conflicting.tags + else '__default__') + + rebulk.rules(BonusTitleRule) + + return rebulk + + +class BonusTitleRule(Rule): + """ + Find bonus title after bonus. + """ + dependency = TitleFromPosition + consequence = AppendMatch + + properties = {'bonus_title': [None]} + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + bonus_number = matches.named('bonus', lambda match: not match.private, index=0) + if bonus_number: + filepath = matches.markers.at_match(bonus_number, lambda marker: marker.name == 'path', 0) + hole = matches.holes(bonus_number.end, filepath.end + 1, formatter=cleanup, index=0) + if hole and hole.value: + hole.name = 'bonus_title' + return hole diff --git a/libs/guessit/rules/properties/cds.py b/libs/guessit/rules/properties/cds.py new file mode 100644 index 000000000..873df6fef --- /dev/null +++ b/libs/guessit/rules/properties/cds.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +cd and cd_count properties +""" +from rebulk.remodule import re + +from rebulk import Rebulk + +from ..common import dash +from ..common.pattern import is_disabled + + +def cds(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'cd')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]) + + rebulk.regex(r'cd-?(?P<cd>\d+)(?:-?of-?(?P<cd_count>\d+))?', + validator={'cd': lambda match: 0 < match.value < 100, + 'cd_count': lambda match: 0 < match.value < 100}, + formatter={'cd': int, 'cd_count': int}, + children=True, + private_parent=True, + properties={'cd': [None], 'cd_count': [None]}) + rebulk.regex(r'(?P<cd_count>\d+)-?cds?', + validator={'cd': lambda match: 0 < match.value < 100, + 'cd_count': lambda match: 0 < match.value < 100}, + formatter={'cd_count': int}, + children=True, + private_parent=True, + properties={'cd': [None], 'cd_count': [None]}) + + return rebulk diff --git a/libs/guessit/rules/properties/container.py b/libs/guessit/rules/properties/container.py new file mode 100644 index 000000000..77599509a --- /dev/null +++ b/libs/guessit/rules/properties/container.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +container property +""" +from rebulk.remodule import re + +from rebulk import Rebulk + +from ..common import seps +from ..common.pattern import is_disabled +from ..common.validators import seps_surround +from ...reutils import build_or_pattern + + +def container(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'container')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE).string_defaults(ignore_case=True) + rebulk.defaults(name='container', + formatter=lambda value: value.strip(seps), + tags=['extension'], + conflict_solver=lambda match, other: other + if other.name in ('source', 'video_codec') or + other.name == 'container' and 'extension' not in other.tags + else '__default__') + + subtitles = config['subtitles'] + info = config['info'] + videos = config['videos'] + torrent = config['torrent'] + nzb = config['nzb'] + + rebulk.regex(r'\.'+build_or_pattern(subtitles)+'$', exts=subtitles, tags=['extension', 'subtitle']) + rebulk.regex(r'\.'+build_or_pattern(info)+'$', exts=info, tags=['extension', 'info']) + rebulk.regex(r'\.'+build_or_pattern(videos)+'$', exts=videos, tags=['extension', 'video']) + rebulk.regex(r'\.'+build_or_pattern(torrent)+'$', exts=torrent, tags=['extension', 'torrent']) + rebulk.regex(r'\.'+build_or_pattern(nzb)+'$', exts=nzb, tags=['extension', 'nzb']) + + rebulk.defaults(name='container', + validator=seps_surround, + formatter=lambda s: s.lower(), + conflict_solver=lambda match, other: match + if other.name in ('source', + 'video_codec') or other.name == 'container' and 'extension' in other.tags + else '__default__') + + rebulk.string(*[sub for sub in subtitles if sub not in ('sub', 'ass')], tags=['subtitle']) + rebulk.string(*videos, tags=['video']) + rebulk.string(*torrent, tags=['torrent']) + rebulk.string(*nzb, tags=['nzb']) + + return rebulk diff --git a/libs/guessit/rules/properties/country.py b/libs/guessit/rules/properties/country.py new file mode 100644 index 000000000..138c80a26 --- /dev/null +++ b/libs/guessit/rules/properties/country.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +country property +""" +# pylint: disable=no-member +import babelfish + +from rebulk import Rebulk +from ..common.pattern import is_disabled +from ..common.words import iter_words + + +def country(config, common_words): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :param common_words: common words + :type common_words: set + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'country')) + rebulk = rebulk.defaults(name='country') + + def find_countries(string, context=None): + """ + Find countries in given string. + """ + allowed_countries = context.get('allowed_countries') if context else None + return CountryFinder(allowed_countries, common_words).find(string) + + rebulk.functional(find_countries, + # Prefer language and any other property over country if not US or GB. + conflict_solver=lambda match, other: match + if other.name != 'language' or match.value not in (babelfish.Country('US'), + babelfish.Country('GB')) + else other, + properties={'country': [None]}, + disabled=lambda context: not context.get('allowed_countries')) + + babelfish.country_converters['guessit'] = GuessitCountryConverter(config['synonyms']) + + return rebulk + + +class GuessitCountryConverter(babelfish.CountryReverseConverter): # pylint: disable=missing-docstring + def __init__(self, synonyms): + self.guessit_exceptions = {} + + for alpha2, synlist in synonyms.items(): + for syn in synlist: + self.guessit_exceptions[syn.lower()] = alpha2 + + @property + def codes(self): # pylint: disable=missing-docstring + return (babelfish.country_converters['name'].codes | + frozenset(babelfish.COUNTRIES.values()) | + frozenset(self.guessit_exceptions.keys())) + + def convert(self, alpha2): + if alpha2 == 'GB': + return 'UK' + return str(babelfish.Country(alpha2)) + + def reverse(self, name): # pylint:disable=arguments-differ + # exceptions come first, as they need to override a potential match + # with any of the other guessers + try: + return self.guessit_exceptions[name.lower()] + except KeyError: + pass + + try: + return babelfish.Country(name.upper()).alpha2 + except ValueError: + pass + + for conv in [babelfish.Country.fromname]: + try: + return conv(name).alpha2 + except babelfish.CountryReverseError: + pass + + raise babelfish.CountryReverseError(name) + + +class CountryFinder(object): + """Helper class to search and return country matches.""" + + def __init__(self, allowed_countries, common_words): + self.allowed_countries = set([l.lower() for l in allowed_countries or []]) + self.common_words = common_words + + def find(self, string): + """Return all matches for country.""" + for word_match in iter_words(string.strip().lower()): + word = word_match.value + if word.lower() in self.common_words: + continue + + try: + country_object = babelfish.Country.fromguessit(word) + if (country_object.name.lower() in self.allowed_countries or + country_object.alpha2.lower() in self.allowed_countries): + yield self._to_rebulk_match(word_match, country_object) + except babelfish.Error: + continue + + @classmethod + def _to_rebulk_match(cls, word, value): + return word.span[0], word.span[1], {'value': value} diff --git a/libs/guessit/rules/properties/crc.py b/libs/guessit/rules/properties/crc.py new file mode 100644 index 000000000..c65ab7b37 --- /dev/null +++ b/libs/guessit/rules/properties/crc.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +crc and uuid properties +""" +from rebulk.remodule import re + +from rebulk import Rebulk +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def crc(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'crc32')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE) + rebulk.defaults(validator=seps_surround) + + rebulk.regex('(?:[a-fA-F]|[0-9]){8}', name='crc32', + conflict_solver=lambda match, other: match + if other.name in ['episode', 'season'] + else '__default__') + + rebulk.functional(guess_idnumber, name='uuid', + conflict_solver=lambda match, other: match + if other.name in ['episode', 'season'] + else '__default__') + return rebulk + + +_DIGIT = 0 +_LETTER = 1 +_OTHER = 2 + +_idnum = re.compile(r'(?P<uuid>[a-zA-Z0-9-]{20,})') # 1.0, (0, 0)) + + +def guess_idnumber(string): + """ + Guess id number function + :param string: + :type string: + :return: + :rtype: + """ + # pylint:disable=invalid-name + ret = [] + + matches = list(_idnum.finditer(string)) + for match in matches: + result = match.groupdict() + switch_count = 0 + switch_letter_count = 0 + letter_count = 0 + last_letter = None + + last = _LETTER + for c in result['uuid']: + if c in '0123456789': + ci = _DIGIT + elif c in 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ': + ci = _LETTER + if c != last_letter: + switch_letter_count += 1 + last_letter = c + letter_count += 1 + else: + ci = _OTHER + + if ci != last: + switch_count += 1 + + last = ci + + # only return the result as probable if we alternate often between + # char type (more likely for hash values than for common words) + switch_ratio = float(switch_count) / len(result['uuid']) + letters_ratio = (float(switch_letter_count) / letter_count) if letter_count > 0 else 1 + + if switch_ratio > 0.4 and letters_ratio > 0.4: + ret.append(match.span()) + + return ret diff --git a/libs/guessit/rules/properties/date.py b/libs/guessit/rules/properties/date.py new file mode 100644 index 000000000..4db121c27 --- /dev/null +++ b/libs/guessit/rules/properties/date.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +date and year properties +""" +from rebulk import Rebulk, RemoveMatch, Rule + +from ..common.date import search_date, valid_year +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def date(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk().defaults(validator=seps_surround) + + rebulk.regex(r"\d{4}", name="year", formatter=int, + disabled=lambda context: is_disabled(context, 'year'), + validator=lambda match: seps_surround(match) and valid_year(match.value)) + + def date_functional(string, context): # pylint:disable=inconsistent-return-statements + """ + Search for date in the string and retrieves match + + :param string: + :return: + """ + + ret = search_date(string, context.get('date_year_first'), context.get('date_day_first')) + if ret: + return ret[0], ret[1], {'value': ret[2]} + + rebulk.functional(date_functional, name="date", properties={'date': [None]}, + disabled=lambda context: is_disabled(context, 'date'), + conflict_solver=lambda match, other: other + if other.name in ('episode', 'season', 'crc32') + else '__default__') + + rebulk.rules(KeepMarkedYearInFilepart) + + return rebulk + + +class KeepMarkedYearInFilepart(Rule): + """ + Keep first years marked with [](){} in filepart, or if no year is marked, ensure it won't override titles. + """ + priority = 64 + consequence = RemoveMatch + + def enabled(self, context): + return not is_disabled(context, 'year') + + def when(self, matches, context): + ret = [] + if len(matches.named('year')) > 1: + for filepart in matches.markers.named('path'): + years = matches.range(filepart.start, filepart.end, lambda match: match.name == 'year') + if len(years) > 1: + group_years = [] + ungroup_years = [] + for year in years: + if matches.markers.at_match(year, lambda marker: marker.name == 'group'): + group_years.append(year) + else: + ungroup_years.append(year) + if group_years and ungroup_years: + ret.extend(ungroup_years) + ret.extend(group_years[1:]) # Keep the first year in marker. + elif not group_years: + ret.append(ungroup_years[0]) # Keep first year for title. + if len(ungroup_years) > 2: + ret.extend(ungroup_years[2:]) + return ret diff --git a/libs/guessit/rules/properties/edition.py b/libs/guessit/rules/properties/edition.py new file mode 100644 index 000000000..822aa4ee3 --- /dev/null +++ b/libs/guessit/rules/properties/edition.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +edition property +""" +from rebulk.remodule import re + +from rebulk import Rebulk +from ..common import dash +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def edition(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'edition')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]).string_defaults(ignore_case=True) + rebulk.defaults(name='edition', validator=seps_surround) + + rebulk.regex('collector', "collector'?s?-edition", 'edition-collector', value='Collector') + rebulk.regex('special-edition', 'edition-special', value='Special', + conflict_solver=lambda match, other: other + if other.name == 'episode_details' and other.value == 'Special' + else '__default__') + rebulk.string('se', value='Special', tags='has-neighbor') + rebulk.string('ddc', value="Director's Definitive Cut") + rebulk.regex('criterion-edition', 'edition-criterion', 'CC', value='Criterion') + rebulk.regex('deluxe', 'deluxe-edition', 'edition-deluxe', value='Deluxe') + rebulk.regex('limited', 'limited-edition', value='Limited', tags=['has-neighbor', 'release-group-prefix']) + rebulk.regex(r'theatrical-cut', r'theatrical-edition', r'theatrical', value='Theatrical') + rebulk.regex(r"director'?s?-cut", r"director'?s?-cut-edition", r"edition-director'?s?-cut", 'DC', + value="Director's Cut") + rebulk.regex('extended', 'extended-?cut', 'extended-?version', + value='Extended', tags=['has-neighbor', 'release-group-prefix']) + rebulk.regex('alternat(e|ive)(?:-?Cut)?', value='Alternative Cut', tags=['has-neighbor', 'release-group-prefix']) + for value in ('Remastered', 'Uncensored', 'Uncut', 'Unrated'): + rebulk.string(value, value=value, tags=['has-neighbor', 'release-group-prefix']) + rebulk.string('Festival', value='Festival', tags=['has-neighbor-before', 'has-neighbor-after']) + rebulk.regex('imax', 'imax-edition', value='IMAX') + rebulk.regex('fan-edit(?:ion)?', 'fan-collection', value='Fan') + rebulk.regex('ultimate-edition', value='Ultimate') + rebulk.regex("ultimate-collector'?s?-edition", value=['Ultimate', 'Collector']) + rebulk.regex('ultimate-fan-edit(?:ion)?', 'ultimate-fan-collection', value=['Ultimate', 'Fan']) + + return rebulk diff --git a/libs/guessit/rules/properties/episode_title.py b/libs/guessit/rules/properties/episode_title.py new file mode 100644 index 000000000..bcf605c0c --- /dev/null +++ b/libs/guessit/rules/properties/episode_title.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Episode title +""" +from collections import defaultdict + +from rebulk import Rebulk, Rule, AppendMatch, RemoveMatch, RenameMatch, POST_PROCESS + +from ..common import seps, title_seps +from ..common.formatters import cleanup +from ..common.pattern import is_disabled +from ..properties.title import TitleFromPosition, TitleBaseRule +from ..properties.type import TypeProcessor + + +def episode_title(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + previous_names = ('episode', 'episode_details', 'episode_count', + 'season', 'season_count', 'date', 'title', 'year') + + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'episode_title')) + rebulk = rebulk.rules(RemoveConflictsWithEpisodeTitle(previous_names), + EpisodeTitleFromPosition(previous_names), + AlternativeTitleReplace(previous_names), + TitleToEpisodeTitle, + Filepart3EpisodeTitle, + Filepart2EpisodeTitle, + RenameEpisodeTitleWhenMovieType) + return rebulk + + +class RemoveConflictsWithEpisodeTitle(Rule): + """ + Remove conflicting matches that might lead to wrong episode_title parsing. + """ + + priority = 64 + consequence = RemoveMatch + + def __init__(self, previous_names): + super(RemoveConflictsWithEpisodeTitle, self).__init__() + self.previous_names = previous_names + self.next_names = ('streaming_service', 'screen_size', 'source', + 'video_codec', 'audio_codec', 'other', 'container') + self.affected_if_holes_after = ('part', ) + self.affected_names = ('part', 'year') + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + for match in matches.range(filepart.start, filepart.end, + predicate=lambda m: m.name in self.affected_names): + before = matches.range(filepart.start, match.start, predicate=lambda m: not m.private, index=-1) + if not before or before.name not in self.previous_names: + continue + + after = matches.range(match.end, filepart.end, predicate=lambda m: not m.private, index=0) + if not after or after.name not in self.next_names: + continue + + group = matches.markers.at_match(match, predicate=lambda m: m.name == 'group', index=0) + + def has_value_in_same_group(current_match, current_group=group): + """Return true if current match has value and belongs to the current group.""" + return current_match.value.strip(seps) and ( + current_group == matches.markers.at_match(current_match, + predicate=lambda mm: mm.name == 'group', index=0) + ) + + holes_before = matches.holes(before.end, match.start, predicate=has_value_in_same_group) + holes_after = matches.holes(match.end, after.start, predicate=has_value_in_same_group) + + if not holes_before and not holes_after: + continue + + if match.name in self.affected_if_holes_after and not holes_after: + continue + + to_remove.append(match) + if match.parent: + to_remove.append(match.parent) + + return to_remove + + +class TitleToEpisodeTitle(Rule): + """ + If multiple different title are found, convert the one following episode number to episode_title. + """ + dependency = TitleFromPosition + + def when(self, matches, context): + titles = matches.named('title') + title_groups = defaultdict(list) + for title in titles: + title_groups[title.value].append(title) + + episode_titles = [] + if len(title_groups) < 2: + return episode_titles + + for title in titles: + if matches.previous(title, lambda match: match.name == 'episode'): + episode_titles.append(title) + + return episode_titles + + def then(self, matches, when_response, context): + for title in when_response: + matches.remove(title) + title.name = 'episode_title' + matches.append(title) + + +class EpisodeTitleFromPosition(TitleBaseRule): + """ + Add episode title match in existing matches + Must run after TitleFromPosition rule. + """ + dependency = TitleToEpisodeTitle + + def __init__(self, previous_names): + super(EpisodeTitleFromPosition, self).__init__('episode_title', ['title']) + self.previous_names = previous_names + + def hole_filter(self, hole, matches): + episode = matches.previous(hole, + lambda previous: any(name in previous.names + for name in self.previous_names), + 0) + + crc32 = matches.named('crc32') + + return episode or crc32 + + def filepart_filter(self, filepart, matches): + # Filepart where title was found. + if matches.range(filepart.start, filepart.end, lambda match: match.name == 'title'): + return True + return False + + def should_remove(self, match, matches, filepart, hole, context): + if match.name == 'episode_details': + return False + return super(EpisodeTitleFromPosition, self).should_remove(match, matches, filepart, hole, context) + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + if matches.named('episode_title'): + return + return super(EpisodeTitleFromPosition, self).when(matches, context) + + +class AlternativeTitleReplace(Rule): + """ + If alternateTitle was found and title is next to episode, season or date, replace it with episode_title. + """ + dependency = EpisodeTitleFromPosition + consequence = RenameMatch + + def __init__(self, previous_names): + super(AlternativeTitleReplace, self).__init__() + self.previous_names = previous_names + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + if matches.named('episode_title'): + return + + alternative_title = matches.range(predicate=lambda match: match.name == 'alternative_title', index=0) + if alternative_title: + main_title = matches.chain_before(alternative_title.start, seps=seps, + predicate=lambda match: 'title' in match.tags, index=0) + if main_title: + episode = matches.previous(main_title, + lambda previous: any(name in previous.names + for name in self.previous_names), + 0) + + crc32 = matches.named('crc32') + + if episode or crc32: + return alternative_title + + def then(self, matches, when_response, context): + matches.remove(when_response) + when_response.name = 'episode_title' + when_response.tags.append('alternative-replaced') + matches.append(when_response) + + +class RenameEpisodeTitleWhenMovieType(Rule): + """ + Rename episode_title by alternative_title when type is movie. + """ + priority = POST_PROCESS + + dependency = TypeProcessor + consequence = RenameMatch + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + if matches.named('episode_title', lambda m: 'alternative-replaced' not in m.tags) \ + and not matches.named('type', lambda m: m.value == 'episode'): + return matches.named('episode_title') + + def then(self, matches, when_response, context): + for match in when_response: + matches.remove(match) + match.name = 'alternative_title' + matches.append(match) + + +class Filepart3EpisodeTitle(Rule): + """ + If we have at least 3 filepart structured like this: + + Serie name/SO1/E01-episode_title.mkv + AAAAAAAAAA/BBB/CCCCCCCCCCCCCCCCCCCC + + If CCCC contains episode and BBB contains seasonNumber + Then title is to be found in AAAA. + """ + consequence = AppendMatch('title') + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + fileparts = matches.markers.named('path') + if len(fileparts) < 3: + return + + filename = fileparts[-1] + directory = fileparts[-2] + subdirectory = fileparts[-3] + + episode_number = matches.range(filename.start, filename.end, lambda match: match.name == 'episode', 0) + if episode_number: + season = matches.range(directory.start, directory.end, lambda match: match.name == 'season', 0) + + if season: + hole = matches.holes(subdirectory.start, subdirectory.end, + formatter=cleanup, seps=title_seps, predicate=lambda match: match.value, + index=0) + if hole: + return hole + + +class Filepart2EpisodeTitle(Rule): + """ + If we have at least 2 filepart structured like this: + + Serie name SO1/E01-episode_title.mkv + AAAAAAAAAAAAA/BBBBBBBBBBBBBBBBBBBBB + + If BBBB contains episode and AAA contains a hole followed by seasonNumber + then title is to be found in AAAA. + + or + + Serie name/SO1E01-episode_title.mkv + AAAAAAAAAA/BBBBBBBBBBBBBBBBBBBBB + + If BBBB contains season and episode and AAA contains a hole + then title is to be found in AAAA. + """ + consequence = AppendMatch('title') + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + fileparts = matches.markers.named('path') + if len(fileparts) < 2: + return + + filename = fileparts[-1] + directory = fileparts[-2] + + episode_number = matches.range(filename.start, filename.end, lambda match: match.name == 'episode', 0) + if episode_number: + season = (matches.range(directory.start, directory.end, lambda match: match.name == 'season', 0) or + matches.range(filename.start, filename.end, lambda match: match.name == 'season', 0)) + if season: + hole = matches.holes(directory.start, directory.end, formatter=cleanup, seps=title_seps, + predicate=lambda match: match.value, index=0) + if hole: + return hole diff --git a/libs/guessit/rules/properties/episodes.py b/libs/guessit/rules/properties/episodes.py new file mode 100644 index 000000000..0f2e173cf --- /dev/null +++ b/libs/guessit/rules/properties/episodes.py @@ -0,0 +1,861 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +episode, season, disc, episode_count, season_count and episode_details properties +""" +import copy +from collections import defaultdict + +from rebulk import Rebulk, RemoveMatch, Rule, AppendMatch, RenameMatch +from rebulk.match import Match +from rebulk.remodule import re +from rebulk.utils import is_iterable + +from .title import TitleFromPosition +from ..common import dash, alt_dash, seps, seps_no_fs +from ..common.formatters import strip +from ..common.numeral import numeral, parse_numeral +from ..common.pattern import is_disabled +from ..common.validators import compose, seps_surround, seps_before, int_coercable +from ...reutils import build_or_pattern + + +def episodes(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + # pylint: disable=too-many-branches,too-many-statements,too-many-locals + def is_season_episode_disabled(context): + """Whether season and episode rules should be enabled.""" + return is_disabled(context, 'episode') or is_disabled(context, 'season') + + rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE).string_defaults(ignore_case=True) + rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator', 'episodeMarker', 'seasonMarker']) + + episode_max_range = config['episode_max_range'] + season_max_range = config['season_max_range'] + + def episodes_season_chain_breaker(matches): + """ + Break chains if there's more than 100 offset between two neighbor values. + :param matches: + :type matches: + :return: + :rtype: + """ + eps = matches.named('episode') + if len(eps) > 1 and abs(eps[-1].value - eps[-2].value) > episode_max_range: + return True + + seasons = matches.named('season') + if len(seasons) > 1 and abs(seasons[-1].value - seasons[-2].value) > season_max_range: + return True + return False + + rebulk.chain_defaults(chain_breaker=episodes_season_chain_breaker) + + def season_episode_conflict_solver(match, other): + """ + Conflict solver for episode/season patterns + + :param match: + :param other: + :return: + """ + if match.name != other.name: + if match.name == 'episode' and other.name == 'year': + return match + if match.name in ('season', 'episode'): + if other.name in ('video_codec', 'audio_codec', 'container', 'date'): + return match + if (other.name == 'audio_channels' and 'weak-audio_channels' not in other.tags + and not match.initiator.children.named(match.name + 'Marker')) or ( + other.name == 'screen_size' and not int_coercable(other.raw)): + + return match + if other.name in ('season', 'episode') and match.initiator != other.initiator: + if (match.initiator.name in ('weak_episode', 'weak_duplicate') + and other.initiator.name in ('weak_episode', 'weak_duplicate')): + return '__default__' + for current in (match, other): + if 'weak-episode' in current.tags or 'x' in current.initiator.raw.lower(): + return current + return '__default__' + + season_words = config['season_words'] + episode_words = config['episode_words'] + of_words = config['of_words'] + all_words = config['all_words'] + season_markers = config['season_markers'] + season_ep_markers = config['season_ep_markers'] + disc_markers = config['disc_markers'] + episode_markers = config['episode_markers'] + range_separators = config['range_separators'] + weak_discrete_separators = list(sep for sep in seps_no_fs if sep not in range_separators) + strong_discrete_separators = config['discrete_separators'] + discrete_separators = strong_discrete_separators + weak_discrete_separators + + max_range_gap = config['max_range_gap'] + + def ordering_validator(match): + """ + Validator for season list. They should be in natural order to be validated. + + episode/season separated by a weak discrete separator should be consecutive, unless a strong discrete separator + or a range separator is present in the chain (1.3&5 is valid, but 1.3-5 is not valid and 1.3.5 is not valid) + """ + values = match.children.to_dict() + if 'season' in values and is_iterable(values['season']): + # Season numbers must be in natural order to be validated. + if not list(sorted(values['season'])) == values['season']: + return False + if 'episode' in values and is_iterable(values['episode']): + # Season numbers must be in natural order to be validated. + if not list(sorted(values['episode'])) == values['episode']: + return False + + def is_consecutive(property_name): + """ + Check if the property season or episode has valid consecutive values. + :param property_name: + :type property_name: + :return: + :rtype: + """ + previous_match = None + valid = True + for current_match in match.children.named(property_name): + if previous_match: + match.children.previous(current_match, + lambda m: m.name == property_name + 'Separator') + separator = match.children.previous(current_match, + lambda m: m.name == property_name + 'Separator', 0) + if separator.raw not in range_separators and separator.raw in weak_discrete_separators: + if not 0 < current_match.value - previous_match.value <= max_range_gap + 1: + valid = False + if separator.raw in strong_discrete_separators: + valid = True + break + previous_match = current_match + return valid + + return is_consecutive('episode') and is_consecutive('season') + + # S01E02, 01x02, S01S02S03 + rebulk.chain(formatter={'season': int, 'episode': int}, + tags=['SxxExx'], + abbreviations=[alt_dash], + children=True, + private_parent=True, + validate_all=True, + validator={'__parent__': ordering_validator}, + conflict_solver=season_episode_conflict_solver, + disabled=is_season_episode_disabled) \ + .regex(build_or_pattern(season_markers, name='seasonMarker') + r'(?P<season>\d+)@?' + + build_or_pattern(episode_markers + disc_markers, name='episodeMarker') + r'@?(?P<episode>\d+)', + validate_all=True, + validator={'__parent__': seps_before}).repeater('+') \ + .regex(build_or_pattern(episode_markers + disc_markers + discrete_separators + range_separators, + name='episodeSeparator', + escape=True) + + r'(?P<episode>\d+)').repeater('*') \ + .chain() \ + .regex(r'(?P<season>\d+)@?' + + build_or_pattern(season_ep_markers, name='episodeMarker') + + r'@?(?P<episode>\d+)', + validate_all=True, + validator={'__parent__': seps_before}) \ + .chain() \ + .regex(r'(?P<season>\d+)@?' + + build_or_pattern(season_ep_markers, name='episodeMarker') + + r'@?(?P<episode>\d+)', + validate_all=True, + validator={'__parent__': seps_before}) \ + .regex(build_or_pattern(season_ep_markers + discrete_separators + range_separators, + name='episodeSeparator', + escape=True) + + r'(?P<episode>\d+)').repeater('*') \ + .chain() \ + .regex(build_or_pattern(season_markers, name='seasonMarker') + r'(?P<season>\d+)', + validate_all=True, + validator={'__parent__': seps_before}) \ + .regex(build_or_pattern(season_markers + discrete_separators + range_separators, + name='seasonSeparator', + escape=True) + + r'(?P<season>\d+)').repeater('*') + + # episode_details property + for episode_detail in ('Special', 'Bonus', 'Pilot', 'Unaired', 'Final'): + rebulk.string(episode_detail, value=episode_detail, name='episode_details', + disabled=lambda context: is_disabled(context, 'episode_details')) + rebulk.regex(r'Extras?', 'Omake', name='episode_details', value='Extras', + disabled=lambda context: is_disabled(context, 'episode_details')) + + def validate_roman(match): + """ + Validate a roman match if surrounded by separators + :param match: + :type match: + :return: + :rtype: + """ + if int_coercable(match.raw): + return True + return seps_surround(match) + + rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator', 'episodeMarker', 'seasonMarker'], + validate_all=True, validator={'__parent__': seps_surround}, children=True, private_parent=True, + conflict_solver=season_episode_conflict_solver) + + rebulk.chain(abbreviations=[alt_dash], + formatter={'season': parse_numeral, 'count': parse_numeral}, + validator={'__parent__': compose(seps_surround, ordering_validator), + 'season': validate_roman, + 'count': validate_roman}, + disabled=lambda context: context.get('type') == 'movie' or is_disabled(context, 'season')) \ + .defaults(validator=None) \ + .regex(build_or_pattern(season_words, name='seasonMarker') + '@?(?P<season>' + numeral + ')') \ + .regex(r'' + build_or_pattern(of_words) + '@?(?P<count>' + numeral + ')').repeater('?') \ + .regex(r'@?' + build_or_pattern(range_separators + discrete_separators + ['@'], + name='seasonSeparator', escape=True) + + r'@?(?P<season>\d+)').repeater('*') + + rebulk.regex(build_or_pattern(episode_words, name='episodeMarker') + r'-?(?P<episode>\d+)' + + r'(?:v(?P<version>\d+))?' + + r'(?:-?' + build_or_pattern(of_words) + r'-?(?P<count>\d+))?', # Episode 4 + abbreviations=[dash], formatter={'episode': int, 'version': int, 'count': int}, + disabled=lambda context: context.get('type') == 'episode' or is_disabled(context, 'episode')) + + rebulk.regex(build_or_pattern(episode_words, name='episodeMarker') + r'-?(?P<episode>' + numeral + ')' + + r'(?:v(?P<version>\d+))?' + + r'(?:-?' + build_or_pattern(of_words) + r'-?(?P<count>\d+))?', # Episode 4 + abbreviations=[dash], + validator={'episode': validate_roman}, + formatter={'episode': parse_numeral, 'version': int, 'count': int}, + disabled=lambda context: context.get('type') != 'episode' or is_disabled(context, 'episode')) + + rebulk.regex(r'S?(?P<season>\d+)-?(?:xE|Ex|E|x)-?(?P<other>' + build_or_pattern(all_words) + ')', + tags=['SxxExx'], + abbreviations=[dash], + validator=None, + formatter={'season': int, 'other': lambda match: 'Complete'}, + disabled=lambda context: is_disabled(context, 'season')) + + # 12, 13 + rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': int}, + disabled=lambda context: context.get('type') == 'movie' or is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'(?P<episode>\d{2})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{2})').repeater('*') + + # 012, 013 + rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': int}, + disabled=lambda context: context.get('type') == 'movie' or is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'0(?P<episode>\d{1,2})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>[x-])0(?P<episode>\d{1,2})').repeater('*') + + # 112, 113 + rebulk.chain(tags=['weak-episode'], + formatter={'episode': int, 'version': int}, + name='weak_episode', + disabled=lambda context: context.get('type') == 'movie' or is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'(?P<episode>\d{3,4})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{3,4})').repeater('*') + + # 1, 2, 3 + rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': int}, + disabled=lambda context: context.get('type') != 'episode' or is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'(?P<episode>\d)') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>[x-])(?P<episode>\d{1,2})').repeater('*') + + # e112, e113 + # TODO: Enhance rebulk for validator to be used globally (season_episode_validator) + rebulk.chain(formatter={'episode': int, 'version': int}, + disabled=lambda context: is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'(?P<episodeMarker>e)(?P<episode>\d{1,4})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>e|x|-)(?P<episode>\d{1,4})').repeater('*') + + # ep 112, ep113, ep112, ep113 + rebulk.chain(abbreviations=[dash], formatter={'episode': int, 'version': int}, + disabled=lambda context: is_disabled(context, 'episode')) \ + .defaults(validator=None) \ + .regex(r'ep-?(?P<episode>\d{1,4})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>ep|e|x|-)(?P<episode>\d{1,4})').repeater('*') + + # cap 112, cap 112_114 + rebulk.chain(abbreviations=[dash], + tags=['see-pattern'], + formatter={'season': int, 'episode': int}, + disabled=is_season_episode_disabled) \ + .defaults(validator=None) \ + .regex(r'(?P<seasonMarker>cap)-?(?P<season>\d{1,2})(?P<episode>\d{2})') \ + .regex(r'(?P<episodeSeparator>-)(?P<season>\d{1,2})(?P<episode>\d{2})').repeater('?') + + # 102, 0102 + rebulk.chain(tags=['weak-episode', 'weak-duplicate'], + formatter={'season': int, 'episode': int, 'version': int}, + name='weak_duplicate', + conflict_solver=season_episode_conflict_solver, + disabled=lambda context: (context.get('episode_prefer_number', False) or + context.get('type') == 'movie') or is_season_episode_disabled(context)) \ + .defaults(validator=None) \ + .regex(r'(?P<season>\d{1,2})(?P<episode>\d{2})') \ + .regex(r'v(?P<version>\d+)').repeater('?') \ + .regex(r'(?P<episodeSeparator>x|-)(?P<episode>\d{2})').repeater('*') + + rebulk.regex(r'v(?P<version>\d+)', children=True, private_parent=True, formatter=int, + disabled=lambda context: is_disabled(context, 'version')) + + rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator']) + + # TODO: List of words + # detached of X count (season/episode) + rebulk.regex(r'(?P<episode>\d+)-?' + build_or_pattern(of_words) + + r'-?(?P<count>\d+)-?' + build_or_pattern(episode_words) + '?', + abbreviations=[dash], children=True, private_parent=True, formatter=int, + disabled=lambda context: is_disabled(context, 'episode')) + + rebulk.regex(r'Minisodes?', name='episode_format', value="Minisode", + disabled=lambda context: is_disabled(context, 'episode_format')) + + rebulk.rules(WeakConflictSolver, RemoveInvalidSeason, RemoveInvalidEpisode, + SeePatternRange(range_separators + ['_']), + EpisodeNumberSeparatorRange(range_separators), + SeasonSeparatorRange(range_separators), RemoveWeakIfMovie, RemoveWeakIfSxxExx, + RemoveWeakDuplicate, EpisodeDetailValidator, RemoveDetachedEpisodeNumber, VersionValidator, + RemoveWeak, RenameToAbsoluteEpisode, CountValidator, EpisodeSingleDigitValidator, RenameToDiscMatch) + + return rebulk + + +class WeakConflictSolver(Rule): + """ + Rule to decide whether weak-episode or weak-duplicate matches should be kept. + + If an anime is detected: + - weak-duplicate matches should be removed + - weak-episode matches should be tagged as anime + Otherwise: + - weak-episode matches are removed unless they're part of an episode range match. + """ + priority = 128 + consequence = [RemoveMatch, AppendMatch] + + def enabled(self, context): + return context.get('type') != 'movie' + + @classmethod + def is_anime(cls, matches): + """Return True if it seems to be an anime. + + Anime characteristics: + - version, crc32 matches + - screen_size inside brackets + - release_group at start and inside brackets + """ + if matches.named('version') or matches.named('crc32'): + return True + + for group in matches.markers.named('group'): + if matches.range(group.start, group.end, predicate=lambda m: m.name == 'screen_size'): + return True + if matches.markers.starting(group.start, predicate=lambda m: m.name == 'path'): + hole = matches.holes(group.start, group.end, index=0) + if hole and hole.raw == group.raw: + return True + + return False + + def when(self, matches, context): + to_remove = [] + to_append = [] + anime_detected = self.is_anime(matches) + for filepart in matches.markers.named('path'): + weak_matches = matches.range(filepart.start, filepart.end, predicate=( + lambda m: m.initiator.name == 'weak_episode')) + weak_dup_matches = matches.range(filepart.start, filepart.end, predicate=( + lambda m: m.initiator.name == 'weak_duplicate')) + if anime_detected: + if weak_matches: + to_remove.extend(weak_dup_matches) + for match in matches.range(filepart.start, filepart.end, predicate=( + lambda m: m.name == 'episode' and m.initiator.name != 'weak_duplicate')): + episode = copy.copy(match) + episode.tags = episode.tags + ['anime'] + to_append.append(episode) + to_remove.append(match) + elif weak_dup_matches: + episodes_in_range = matches.range(filepart.start, filepart.end, predicate=( + lambda m: + m.name == 'episode' and m.initiator.name == 'weak_episode' + and m.initiator.children.named('episodeSeparator') + )) + if not episodes_in_range and not matches.range(filepart.start, filepart.end, + predicate=lambda m: 'SxxExx' in m.tags): + to_remove.extend(weak_matches) + else: + for match in episodes_in_range: + episode = copy.copy(match) + episode.tags = [] + to_append.append(episode) + to_remove.append(match) + + if to_append: + to_remove.extend(weak_dup_matches) + + return to_remove, to_append + + +class CountValidator(Rule): + """ + Validate count property and rename it + """ + priority = 64 + consequence = [RemoveMatch, RenameMatch('episode_count'), RenameMatch('season_count')] + + properties = {'episode_count': [None], 'season_count': [None]} + + def when(self, matches, context): + to_remove = [] + episode_count = [] + season_count = [] + + for count in matches.named('count'): + previous = matches.previous(count, lambda match: match.name in ['episode', 'season'], 0) + if previous: + if previous.name == 'episode': + episode_count.append(count) + elif previous.name == 'season': + season_count.append(count) + else: + to_remove.append(count) + return to_remove, episode_count, season_count + + +class SeePatternRange(Rule): + """ + Create matches for episode range for SEE pattern. E.g.: Cap.102_104 + """ + priority = 128 + consequence = [RemoveMatch, AppendMatch] + + def __init__(self, range_separators): + super(SeePatternRange, self).__init__() + self.range_separators = range_separators + + def when(self, matches, context): + to_remove = [] + to_append = [] + + for separator in matches.tagged('see-pattern', lambda m: m.name == 'episodeSeparator'): + previous_match = matches.previous(separator, lambda m: m.name == 'episode' and 'see-pattern' in m.tags, 0) + next_match = matches.next(separator, lambda m: m.name == 'season' and 'see-pattern' in m.tags, 0) + if not next_match: + continue + + next_match = matches.next(next_match, lambda m: m.name == 'episode' and 'see-pattern' in m.tags, 0) + if previous_match and next_match and separator.value in self.range_separators: + to_remove.append(next_match) + + for episode_number in range(previous_match.value + 1, next_match.value + 1): + match = copy.copy(next_match) + match.value = episode_number + to_append.append(match) + + to_remove.append(separator) + + return to_remove, to_append + + +class AbstractSeparatorRange(Rule): + """ + Remove separator matches and create matches for season range. + """ + priority = 128 + consequence = [RemoveMatch, AppendMatch] + + def __init__(self, range_separators, property_name): + super(AbstractSeparatorRange, self).__init__() + self.range_separators = range_separators + self.property_name = property_name + + def when(self, matches, context): + to_remove = [] + to_append = [] + + for separator in matches.named(self.property_name + 'Separator'): + previous_match = matches.previous(separator, lambda m: m.name == self.property_name, 0) + next_match = matches.next(separator, lambda m: m.name == self.property_name, 0) + initiator = separator.initiator + + if previous_match and next_match and separator.value in self.range_separators: + to_remove.append(next_match) + for episode_number in range(previous_match.value + 1, next_match.value): + match = copy.copy(next_match) + match.value = episode_number + initiator.children.append(match) + to_append.append(match) + to_append.append(next_match) + to_remove.append(separator) + + previous_match = None + for next_match in matches.named(self.property_name): + if previous_match: + separator = matches.input_string[previous_match.initiator.end:next_match.initiator.start] + if separator not in self.range_separators: + separator = strip(separator) + if separator in self.range_separators: + initiator = previous_match.initiator + for episode_number in range(previous_match.value + 1, next_match.value): + match = copy.copy(next_match) + match.value = episode_number + initiator.children.append(match) + to_append.append(match) + to_append.append(Match(previous_match.end, next_match.start - 1, + name=self.property_name + 'Separator', + private=True, + input_string=matches.input_string)) + to_remove.append(next_match) # Remove and append match to support proper ordering + to_append.append(next_match) + + previous_match = next_match + + return to_remove, to_append + + +class RenameToAbsoluteEpisode(Rule): + """ + Rename episode to absolute_episodes. + + Absolute episodes are only used if two groups of episodes are detected: + S02E04-06 25-27 + 25-27 S02E04-06 + 2x04-06 25-27 + 28. Anime Name S02E05 + The matches in the group with higher episode values are renamed to absolute_episode. + """ + + consequence = RenameMatch('absolute_episode') + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + initiators = set([match.initiator for match in matches.named('episode') + if len(match.initiator.children.named('episode')) > 1]) + if len(initiators) != 2: + ret = [] + for filepart in matches.markers.named('path'): + if matches.range(filepart.start + 1, filepart.end, predicate=lambda m: m.name == 'episode'): + ret.extend( + matches.starting(filepart.start, predicate=lambda m: m.initiator.name == 'weak_episode')) + return ret + + initiators = sorted(initiators, key=lambda item: item.end) + if not matches.holes(initiators[0].end, initiators[1].start, predicate=lambda m: m.raw.strip(seps)): + first_range = matches.named('episode', predicate=lambda m: m.initiator == initiators[0]) + second_range = matches.named('episode', predicate=lambda m: m.initiator == initiators[1]) + if len(first_range) == len(second_range): + if second_range[0].value > first_range[0].value: + return second_range + if first_range[0].value > second_range[0].value: + return first_range + + +class EpisodeNumberSeparatorRange(AbstractSeparatorRange): + """ + Remove separator matches and create matches for episoderNumber range. + """ + + def __init__(self, range_separators): + super(EpisodeNumberSeparatorRange, self).__init__(range_separators, "episode") + + +class SeasonSeparatorRange(AbstractSeparatorRange): + """ + Remove separator matches and create matches for season range. + """ + + def __init__(self, range_separators): + super(SeasonSeparatorRange, self).__init__(range_separators, "season") + + +class RemoveWeakIfMovie(Rule): + """ + Remove weak-episode tagged matches if it seems to be a movie. + """ + priority = 64 + consequence = RemoveMatch + + def enabled(self, context): + return context.get('type') != 'episode' + + def when(self, matches, context): + to_remove = [] + to_ignore = set() + remove = False + for filepart in matches.markers.named('path'): + year = matches.range(filepart.start, filepart.end, predicate=lambda m: m.name == 'year', index=0) + if year: + remove = True + next_match = matches.range(year.end, filepart.end, predicate=lambda m: m.private, index=0) + if (next_match and not matches.holes(year.end, next_match.start, predicate=lambda m: m.raw.strip(seps)) + and not matches.at_match(next_match, predicate=lambda m: m.name == 'year')): + to_ignore.add(next_match.initiator) + + to_ignore.update(matches.range(filepart.start, filepart.end, + predicate=lambda m: len(m.children.named('episode')) > 1)) + + to_remove.extend(matches.conflicting(year)) + if remove: + to_remove.extend(matches.tagged('weak-episode', predicate=( + lambda m: m.initiator not in to_ignore and 'anime' not in m.tags))) + + return to_remove + + +class RemoveWeak(Rule): + """ + Remove weak-episode matches which appears after video, source, and audio matches. + """ + priority = 16 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + weaks = matches.range(filepart.start, filepart.end, predicate=lambda m: 'weak-episode' in m.tags) + if weaks: + previous = matches.previous(weaks[0], predicate=lambda m: m.name in ( + 'audio_codec', 'screen_size', 'streaming_service', 'source', 'video_profile', + 'audio_channels', 'audio_profile'), index=0) + if previous and not matches.holes( + previous.end, weaks[0].start, predicate=lambda m: m.raw.strip(seps)): + to_remove.extend(weaks) + return to_remove + + +class RemoveWeakIfSxxExx(Rule): + """ + Remove weak-episode tagged matches if SxxExx pattern is matched. + + Weak episodes at beginning of filepart are kept. + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + if matches.range(filepart.start, filepart.end, + predicate=lambda m: not m.private and 'SxxExx' in m.tags): + for match in matches.range(filepart.start, filepart.end, predicate=lambda m: 'weak-episode' in m.tags): + if match.start != filepart.start or match.initiator.name != 'weak_episode': + to_remove.append(match) + return to_remove + + +class RemoveInvalidSeason(Rule): + """ + Remove invalid season matches. + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + strong_season = matches.range(filepart.start, filepart.end, index=0, + predicate=lambda m: m.name == 'season' + and not m.private and 'SxxExx' in m.tags) + if strong_season: + if strong_season.initiator.children.named('episode'): + for season in matches.range(strong_season.end, filepart.end, + predicate=lambda m: m.name == 'season' and not m.private): + # remove weak season or seasons without episode matches + if 'SxxExx' not in season.tags or not season.initiator.children.named('episode'): + if season.initiator: + to_remove.append(season.initiator) + to_remove.extend(season.initiator.children) + else: + to_remove.append(season) + + return to_remove + + +class RemoveInvalidEpisode(Rule): + """ + Remove invalid episode matches. + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + strong_episode = matches.range(filepart.start, filepart.end, index=0, + predicate=lambda m: m.name == 'episode' + and not m.private and 'SxxExx' in m.tags) + if strong_episode: + strong_ep_marker = RemoveInvalidEpisode.get_episode_prefix(matches, strong_episode) + for episode in matches.range(strong_episode.end, filepart.end, + predicate=lambda m: m.name == 'episode' and not m.private): + ep_marker = RemoveInvalidEpisode.get_episode_prefix(matches, episode) + if strong_ep_marker and ep_marker and strong_ep_marker.value.lower() != ep_marker.value.lower(): + if episode.initiator: + to_remove.append(episode.initiator) + to_remove.extend(episode.initiator.children) + else: + to_remove.append(ep_marker) + to_remove.append(episode) + + return to_remove + + @staticmethod + def get_episode_prefix(matches, episode): + """ + Return episode prefix: episodeMarker or episodeSeparator + """ + return matches.previous(episode, index=0, + predicate=lambda m: m.name in ('episodeMarker', 'episodeSeparator')) + + +class RemoveWeakDuplicate(Rule): + """ + Remove weak-duplicate tagged matches if duplicate patterns, for example The 100.109 + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + patterns = defaultdict(list) + for match in reversed(matches.range(filepart.start, filepart.end, + predicate=lambda m: 'weak-duplicate' in m.tags)): + if match.pattern in patterns[match.name]: + to_remove.append(match) + else: + patterns[match.name].append(match.pattern) + return to_remove + + +class EpisodeDetailValidator(Rule): + """ + Validate episode_details if they are detached or next to season or episode. + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + ret = [] + for detail in matches.named('episode_details'): + if not seps_surround(detail) \ + and not matches.previous(detail, lambda match: match.name in ['season', 'episode']) \ + and not matches.next(detail, lambda match: match.name in ['season', 'episode']): + ret.append(detail) + return ret + + +class RemoveDetachedEpisodeNumber(Rule): + """ + If multiple episode are found, remove those that are not detached from a range and less than 10. + + Fairy Tail 2 - 16-20, 2 should be removed. + """ + priority = 64 + consequence = RemoveMatch + dependency = [RemoveWeakIfSxxExx, RemoveWeakDuplicate] + + def when(self, matches, context): + ret = [] + + episode_numbers = [] + episode_values = set() + for match in matches.named('episode', lambda m: not m.private and 'weak-episode' in m.tags): + if match.value not in episode_values: + episode_numbers.append(match) + episode_values.add(match.value) + + episode_numbers = list(sorted(episode_numbers, key=lambda m: m.value)) + if len(episode_numbers) > 1 and \ + episode_numbers[0].value < 10 and \ + episode_numbers[1].value - episode_numbers[0].value != 1: + parent = episode_numbers[0] + while parent: # TODO: Add a feature in rebulk to avoid this ... + ret.append(parent) + parent = parent.parent + return ret + + +class VersionValidator(Rule): + """ + Validate version if previous match is episode or if surrounded by separators. + """ + priority = 64 + dependency = [RemoveWeakIfMovie, RemoveWeakIfSxxExx] + consequence = RemoveMatch + + def when(self, matches, context): + ret = [] + for version in matches.named('version'): + episode_number = matches.previous(version, lambda match: match.name == 'episode', 0) + if not episode_number and not seps_surround(version.initiator): + ret.append(version) + return ret + + +class EpisodeSingleDigitValidator(Rule): + """ + Remove single digit episode when inside a group that doesn't own title. + """ + dependency = [TitleFromPosition] + + consequence = RemoveMatch + + def when(self, matches, context): + ret = [] + for episode in matches.named('episode', lambda match: len(match.initiator) == 1): + group = matches.markers.at_match(episode, lambda marker: marker.name == 'group', index=0) + if group: + if not matches.range(*group.span, predicate=lambda match: match.name == 'title'): + ret.append(episode) + return ret + + +class RenameToDiscMatch(Rule): + """ + Rename episodes detected with `d` episodeMarkers to `disc`. + """ + + consequence = [RenameMatch('disc'), RenameMatch('discMarker'), RemoveMatch] + + def when(self, matches, context): + discs = [] + markers = [] + to_remove = [] + + disc_disabled = is_disabled(context, 'disc') + + for marker in matches.named('episodeMarker', predicate=lambda m: m.value.lower() == 'd'): + if disc_disabled: + to_remove.append(marker) + to_remove.extend(marker.initiator.children) + continue + + markers.append(marker) + discs.extend(sorted(marker.initiator.children.named('episode'), key=lambda m: m.value)) + + return discs, markers, to_remove diff --git a/libs/guessit/rules/properties/film.py b/libs/guessit/rules/properties/film.py new file mode 100644 index 000000000..3c7e6c0ff --- /dev/null +++ b/libs/guessit/rules/properties/film.py @@ -0,0 +1,48 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +film property +""" +from rebulk import Rebulk, AppendMatch, Rule +from rebulk.remodule import re + +from ..common.formatters import cleanup +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def film(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, validate_all=True, validator={'__parent__': seps_surround}) + + rebulk.regex(r'f(\d{1,2})', name='film', private_parent=True, children=True, formatter=int, + disabled=lambda context: is_disabled(context, 'film')) + + rebulk.rules(FilmTitleRule) + + return rebulk + + +class FilmTitleRule(Rule): + """ + Rule to find out film_title (hole after film property + """ + consequence = AppendMatch + + properties = {'film_title': [None]} + + def enabled(self, context): + return not is_disabled(context, 'film_title') + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + bonus_number = matches.named('film', lambda match: not match.private, index=0) + if bonus_number: + filepath = matches.markers.at_match(bonus_number, lambda marker: marker.name == 'path', 0) + hole = matches.holes(filepath.start, bonus_number.start + 1, formatter=cleanup, index=0) + if hole and hole.value: + hole.name = 'film_title' + return hole diff --git a/libs/guessit/rules/properties/language.py b/libs/guessit/rules/properties/language.py new file mode 100644 index 000000000..0cc4b94a8 --- /dev/null +++ b/libs/guessit/rules/properties/language.py @@ -0,0 +1,503 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +language and subtitle_language properties +""" +# pylint: disable=no-member +import copy +from collections import defaultdict, namedtuple + +import babelfish +from rebulk import Rebulk, Rule, RemoveMatch, RenameMatch +from rebulk.remodule import re + +from ..common import seps +from ..common.pattern import is_disabled +from ..common.words import iter_words +from ..common.validators import seps_surround + + +def language(config, common_words): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :param common_words: common words + :type common_words: set + :return: Created Rebulk object + :rtype: Rebulk + """ + subtitle_both = config['subtitle_affixes'] + subtitle_prefixes = sorted(subtitle_both + config['subtitle_prefixes'], key=length_comparator) + subtitle_suffixes = sorted(subtitle_both + config['subtitle_suffixes'], key=length_comparator) + lang_both = config['language_affixes'] + lang_prefixes = sorted(lang_both + config['language_prefixes'], key=length_comparator) + lang_suffixes = sorted(lang_both + config['language_suffixes'], key=length_comparator) + weak_affixes = frozenset(config['weak_affixes']) + + rebulk = Rebulk(disabled=lambda context: (is_disabled(context, 'language') and + is_disabled(context, 'subtitle_language'))) + + rebulk.string(*subtitle_prefixes, name="subtitle_language.prefix", ignore_case=True, private=True, + validator=seps_surround, tags=['release-group-prefix'], + disabled=lambda context: is_disabled(context, 'subtitle_language')) + rebulk.string(*subtitle_suffixes, name="subtitle_language.suffix", ignore_case=True, private=True, + validator=seps_surround, + disabled=lambda context: is_disabled(context, 'subtitle_language')) + rebulk.string(*lang_suffixes, name="language.suffix", ignore_case=True, private=True, + validator=seps_surround, tags=['source-suffix'], + disabled=lambda context: is_disabled(context, 'language')) + + def find_languages(string, context=None): + """Find languages in the string + + :return: list of tuple (property, Language, lang_word, word) + """ + return LanguageFinder(context, subtitle_prefixes, subtitle_suffixes, + lang_prefixes, lang_suffixes, weak_affixes).find(string) + + rebulk.functional(find_languages, + properties={'language': [None]}, + disabled=lambda context: not context.get('allowed_languages')) + rebulk.rules(SubtitleExtensionRule, + SubtitlePrefixLanguageRule, + SubtitleSuffixLanguageRule, + RemoveLanguage, + RemoveInvalidLanguages(common_words)) + + babelfish.language_converters['guessit'] = GuessitConverter(config['synonyms']) + + return rebulk + + +UNDETERMINED = babelfish.Language('und') + + +class GuessitConverter(babelfish.LanguageReverseConverter): # pylint: disable=missing-docstring + _with_country_regexp = re.compile(r'(.*)\((.*)\)') + _with_country_regexp2 = re.compile(r'(.*)-(.*)') + + def __init__(self, synonyms): + self.guessit_exceptions = {} + for code, synlist in synonyms.items(): + if '_' in code: + (alpha3, country) = code.split('_') + else: + (alpha3, country) = (code, None) + for syn in synlist: + self.guessit_exceptions[syn.lower()] = (alpha3, country, None) + + @property + def codes(self): # pylint: disable=missing-docstring + return (babelfish.language_converters['alpha3b'].codes | + babelfish.language_converters['alpha2'].codes | + babelfish.language_converters['name'].codes | + babelfish.language_converters['opensubtitles'].codes | + babelfish.country_converters['name'].codes | + frozenset(self.guessit_exceptions.keys())) + + def convert(self, alpha3, country=None, script=None): + return str(babelfish.Language(alpha3, country, script)) + + def reverse(self, name): # pylint:disable=arguments-differ + name = name.lower() + # exceptions come first, as they need to override a potential match + # with any of the other guessers + try: + return self.guessit_exceptions[name] + except KeyError: + pass + + for conv in [babelfish.Language, + babelfish.Language.fromalpha3b, + babelfish.Language.fromalpha2, + babelfish.Language.fromname, + babelfish.Language.fromopensubtitles, + babelfish.Language.fromietf]: + try: + reverse = conv(name) + return reverse.alpha3, reverse.country, reverse.script + except (ValueError, babelfish.LanguageReverseError): + pass + + raise babelfish.LanguageReverseError(name) + + +def length_comparator(value): + """ + Return value length. + """ + return len(value) + + +_LanguageMatch = namedtuple('_LanguageMatch', ['property_name', 'word', 'lang']) + + +class LanguageWord(object): + """ + Extension to the Word namedtuple in order to create compound words. + + E.g.: pt-BR, soft subtitles, custom subs + """ + + def __init__(self, start, end, value, input_string, next_word=None): + self.start = start + self.end = end + self.value = value + self.input_string = input_string + self.next_word = next_word + + @property + def extended_word(self): # pylint:disable=inconsistent-return-statements + """ + Return the extended word for this instance, if any. + """ + if self.next_word: + separator = self.input_string[self.end:self.next_word.start] + next_separator = self.input_string[self.next_word.end:self.next_word.end + 1] + + if (separator == '-' and separator != next_separator) or separator in (' ', '.'): + value = self.input_string[self.start:self.next_word.end].replace('.', ' ') + + return LanguageWord(self.start, self.next_word.end, value, self.input_string, self.next_word.next_word) + + def __repr__(self): + return '<({start},{end}): {value}'.format(start=self.start, end=self.end, value=self.value) + + +def to_rebulk_match(language_match): + """ + Convert language match to rebulk Match: start, end, dict + """ + word = language_match.word + start = word.start + end = word.end + name = language_match.property_name + if language_match.lang == UNDETERMINED: + return start, end, { + 'name': name, + 'value': word.value.lower(), + 'formatter': babelfish.Language, + 'tags': ['weak-language'] + } + + return start, end, { + 'name': name, + 'value': language_match.lang + } + + +class LanguageFinder(object): + """ + Helper class to search and return language matches: 'language' and 'subtitle_language' properties + """ + + def __init__(self, context, + subtitle_prefixes, subtitle_suffixes, + lang_prefixes, lang_suffixes, weak_affixes): + allowed_languages = context.get('allowed_languages') if context else None + self.allowed_languages = set([l.lower() for l in allowed_languages or []]) + self.weak_affixes = weak_affixes + self.prefixes_map = {} + self.suffixes_map = {} + + if not is_disabled(context, 'subtitle_language'): + self.prefixes_map['subtitle_language'] = subtitle_prefixes + self.suffixes_map['subtitle_language'] = subtitle_suffixes + + self.prefixes_map['language'] = lang_prefixes + self.suffixes_map['language'] = lang_suffixes + + def find(self, string): + """ + Return all matches for language and subtitle_language. + + Undetermined language matches are removed if a regular language is found. + Multi language matches are removed if there are only undetermined language matches + """ + regular_lang_map = defaultdict(set) + undetermined_map = defaultdict(set) + multi_map = defaultdict(set) + + for match in self.iter_language_matches(string): + key = match.property_name + if match.lang == UNDETERMINED: + undetermined_map[key].add(match) + elif match.lang == 'mul': + multi_map[key].add(match) + else: + regular_lang_map[key].add(match) + + for key, values in multi_map.items(): + if key in regular_lang_map or key not in undetermined_map: + for value in values: + yield to_rebulk_match(value) + + for key, values in undetermined_map.items(): + if key not in regular_lang_map: + for value in values: + yield to_rebulk_match(value) + + for values in regular_lang_map.values(): + for value in values: + yield to_rebulk_match(value) + + def iter_language_matches(self, string): + """ + Return language matches for the given string. + """ + candidates = [] + previous = None + for word in iter_words(string): + language_word = LanguageWord(start=word.span[0], end=word.span[1], value=word.value, input_string=string) + if previous: + previous.next_word = language_word + candidates.append(previous) + previous = language_word + if previous: + candidates.append(previous) + + for candidate in candidates: + for match in self.iter_matches_for_candidate(candidate): + yield match + + def iter_matches_for_candidate(self, language_word): + """ + Return language matches for the given candidate word. + """ + tuples = [ + (language_word, language_word.next_word, + self.prefixes_map, + lambda string, prefix: string.startswith(prefix), + lambda string, prefix: string[len(prefix):]), + (language_word.next_word, language_word, + self.suffixes_map, + lambda string, suffix: string.endswith(suffix), + lambda string, suffix: string[:len(string) - len(suffix)]) + ] + + for word, fallback_word, affixes, is_affix, strip_affix in tuples: + if not word: + continue + + match = self.find_match_for_word(word, fallback_word, affixes, is_affix, strip_affix) + if match: + yield match + + match = self.find_language_match_for_word(language_word) + if match: + yield match + + def find_match_for_word(self, word, fallback_word, affixes, is_affix, strip_affix): # pylint:disable=inconsistent-return-statements + """ + Return the language match for the given word and affixes. + """ + for current_word in (word.extended_word, word): + if not current_word: + continue + + word_lang = current_word.value.lower() + + for key, parts in affixes.items(): + for part in parts: + if not is_affix(word_lang, part): + continue + + match = None + value = strip_affix(word_lang, part) + if not value: + if fallback_word and ( + abs(fallback_word.start - word.end) <= 1 or abs(word.start - fallback_word.end) <= 1): + match = self.find_language_match_for_word(fallback_word, key=key) + + if not match and part not in self.weak_affixes: + match = self.create_language_match(key, LanguageWord(current_word.start, current_word.end, + 'und', current_word.input_string)) + else: + match = self.create_language_match(key, LanguageWord(current_word.start, current_word.end, + value, current_word.input_string)) + + if match: + return match + + def find_language_match_for_word(self, word, key='language'): # pylint:disable=inconsistent-return-statements + """ + Return the language match for the given word. + """ + for current_word in (word.extended_word, word): + if current_word: + match = self.create_language_match(key, current_word) + if match: + return match + + def create_language_match(self, key, word): # pylint:disable=inconsistent-return-statements + """ + Create a LanguageMatch for a given word + """ + lang = self.parse_language(word.value.lower()) + + if lang is not None: + return _LanguageMatch(property_name=key, word=word, lang=lang) + + def parse_language(self, lang_word): # pylint:disable=inconsistent-return-statements + """ + Parse the lang_word into a valid Language. + + Multi and Undetermined languages are also valid languages. + """ + try: + lang = babelfish.Language.fromguessit(lang_word) + if ((hasattr(lang, 'name') and lang.name.lower() in self.allowed_languages) or + (hasattr(lang, 'alpha2') and lang.alpha2.lower() in self.allowed_languages) or + lang.alpha3.lower() in self.allowed_languages): + return lang + + except babelfish.Error: + pass + + +class SubtitlePrefixLanguageRule(Rule): + """ + Convert language guess as subtitle_language if previous match is a subtitle language prefix + """ + consequence = RemoveMatch + + properties = {'subtitle_language': [None]} + + def enabled(self, context): + return not is_disabled(context, 'subtitle_language') + + def when(self, matches, context): + to_rename = [] + to_remove = matches.named('subtitle_language.prefix') + for lang in matches.named('language'): + prefix = matches.previous(lang, lambda match: match.name == 'subtitle_language.prefix', 0) + if not prefix: + group_marker = matches.markers.at_match(lang, lambda marker: marker.name == 'group', 0) + if group_marker: + # Find prefix if placed just before the group + prefix = matches.previous(group_marker, lambda match: match.name == 'subtitle_language.prefix', + 0) + if not prefix: + # Find prefix if placed before in the group + prefix = matches.range(group_marker.start, lang.start, + lambda match: match.name == 'subtitle_language.prefix', 0) + if prefix: + to_rename.append((prefix, lang)) + to_remove.extend(matches.conflicting(lang)) + if prefix in to_remove: + to_remove.remove(prefix) + return to_rename, to_remove + + def then(self, matches, when_response, context): + to_rename, to_remove = when_response + super(SubtitlePrefixLanguageRule, self).then(matches, to_remove, context) + for prefix, match in to_rename: + # Remove suffix equivalent of prefix. + suffix = copy.copy(prefix) + suffix.name = 'subtitle_language.suffix' + if suffix in matches: + matches.remove(suffix) + matches.remove(match) + match.name = 'subtitle_language' + matches.append(match) + + +class SubtitleSuffixLanguageRule(Rule): + """ + Convert language guess as subtitle_language if next match is a subtitle language suffix + """ + dependency = SubtitlePrefixLanguageRule + consequence = RemoveMatch + + properties = {'subtitle_language': [None]} + + def enabled(self, context): + return not is_disabled(context, 'subtitle_language') + + def when(self, matches, context): + to_append = [] + to_remove = matches.named('subtitle_language.suffix') + for lang in matches.named('language'): + suffix = matches.next(lang, lambda match: match.name == 'subtitle_language.suffix', 0) + if suffix: + to_append.append(lang) + if suffix in to_remove: + to_remove.remove(suffix) + return to_append, to_remove + + def then(self, matches, when_response, context): + to_rename, to_remove = when_response + super(SubtitleSuffixLanguageRule, self).then(matches, to_remove, context) + for match in to_rename: + matches.remove(match) + match.name = 'subtitle_language' + matches.append(match) + + +class SubtitleExtensionRule(Rule): + """ + Convert language guess as subtitle_language if next match is a subtitle extension. + + Since it's a strong match, it also removes any conflicting source with it. + """ + consequence = [RemoveMatch, RenameMatch('subtitle_language')] + + properties = {'subtitle_language': [None]} + + def enabled(self, context): + return not is_disabled(context, 'subtitle_language') + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + subtitle_extension = matches.named('container', + lambda match: 'extension' in match.tags and 'subtitle' in match.tags, + 0) + if subtitle_extension: + subtitle_lang = matches.previous(subtitle_extension, lambda match: match.name == 'language', 0) + if subtitle_lang: + for weak in matches.named('subtitle_language', predicate=lambda m: 'weak-language' in m.tags): + weak.private = True + + return matches.conflicting(subtitle_lang, lambda m: m.name == 'source'), subtitle_lang + + +class RemoveLanguage(Rule): + """Remove language matches that were not converted to subtitle_language when language is disabled.""" + + consequence = RemoveMatch + + def enabled(self, context): + return is_disabled(context, 'language') + + def when(self, matches, context): + return matches.named('language') + + +class RemoveInvalidLanguages(Rule): + """Remove language matches that matches the blacklisted common words.""" + + consequence = RemoveMatch + + def __init__(self, common_words): + """Constructor.""" + super(RemoveInvalidLanguages, self).__init__() + self.common_words = common_words + + def when(self, matches, context): + to_remove = [] + for match in matches.range(0, len(matches.input_string), + predicate=lambda m: m.name in ('language', 'subtitle_language')): + if match.raw.lower() not in self.common_words: + continue + + group = matches.markers.at_match(match, index=0, predicate=lambda m: m.name == 'group') + if group and ( + not matches.range( + group.start, group.end, predicate=lambda m: m.name not in ('language', 'subtitle_language') + ) and (not matches.holes(group.start, group.end, predicate=lambda m: m.value.strip(seps)))): + continue + + to_remove.append(match) + + return to_remove diff --git a/libs/guessit/rules/properties/mimetype.py b/libs/guessit/rules/properties/mimetype.py new file mode 100644 index 000000000..f9e642ffa --- /dev/null +++ b/libs/guessit/rules/properties/mimetype.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +mimetype property +""" +import mimetypes + +from rebulk import Rebulk, CustomRule, POST_PROCESS +from rebulk.match import Match + +from ..common.pattern import is_disabled +from ...rules.processors import Processors + + +def mimetype(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'mimetype')) + rebulk.rules(Mimetype) + + return rebulk + + +class Mimetype(CustomRule): + """ + Mimetype post processor + :param matches: + :type matches: + :return: + :rtype: + """ + priority = POST_PROCESS + + dependency = Processors + + def when(self, matches, context): + mime, _ = mimetypes.guess_type(matches.input_string, strict=False) + return mime + + def then(self, matches, when_response, context): + mime = when_response + matches.append(Match(len(matches.input_string), len(matches.input_string), name='mimetype', value=mime)) + + @property + def properties(self): + """ + Properties for this rule. + """ + return {'mimetype': [None]} diff --git a/libs/guessit/rules/properties/other.py b/libs/guessit/rules/properties/other.py new file mode 100644 index 000000000..b3d49caa5 --- /dev/null +++ b/libs/guessit/rules/properties/other.py @@ -0,0 +1,344 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +other property +""" +import copy + +from rebulk import Rebulk, Rule, RemoveMatch, RenameMatch, POST_PROCESS, AppendMatch +from rebulk.remodule import re + +from ..common import dash +from ..common import seps +from ..common.pattern import is_disabled +from ..common.validators import seps_after, seps_before, seps_surround, compose +from ...reutils import build_or_pattern +from ...rules.common.formatters import raw_cleanup + + +def other(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'other')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]).string_defaults(ignore_case=True) + rebulk.defaults(name="other", validator=seps_surround) + + rebulk.regex('Audio-?Fix', 'Audio-?Fixed', value='Audio Fixed') + rebulk.regex('Sync-?Fix', 'Sync-?Fixed', value='Sync Fixed') + rebulk.regex('Dual', 'Dual-?Audio', value='Dual Audio') + rebulk.regex('ws', 'wide-?screen', value='Widescreen') + rebulk.regex('Re-?Enc(?:oded)?', value='Reencoded') + + rebulk.string('Real', 'Fix', 'Fixed', value='Proper', tags=['has-neighbor-before', 'has-neighbor-after']) + rebulk.string('Proper', 'Repack', 'Rerip', 'Dirfix', 'Nfofix', 'Prooffix', value='Proper', + tags=['streaming_service.prefix', 'streaming_service.suffix']) + rebulk.regex('(?:Proof-?)?Sample-?Fix', value='Proper', + tags=['streaming_service.prefix', 'streaming_service.suffix']) + rebulk.string('Fansub', value='Fan Subtitled', tags='has-neighbor') + rebulk.string('Fastsub', value='Fast Subtitled', tags='has-neighbor') + + season_words = build_or_pattern(["seasons?", "series?"]) + complete_articles = build_or_pattern(["The"]) + + def validate_complete(match): + """ + Make sure season word is are defined. + :param match: + :type match: + :return: + :rtype: + """ + children = match.children + if not children.named('completeWordsBefore') and not children.named('completeWordsAfter'): + return False + return True + + rebulk.regex('(?P<completeArticle>' + complete_articles + '-)?' + + '(?P<completeWordsBefore>' + season_words + '-)?' + + 'Complete' + '(?P<completeWordsAfter>-' + season_words + ')?', + private_names=['completeArticle', 'completeWordsBefore', 'completeWordsAfter'], + value={'other': 'Complete'}, + tags=['release-group-prefix'], + validator={'__parent__': compose(seps_surround, validate_complete)}) + rebulk.string('R5', value='Region 5') + rebulk.string('RC', value='Region C') + rebulk.regex('Pre-?Air', value='Preair') + rebulk.regex('(?:PS-?)?Vita', value='PS Vita') + rebulk.regex('(HD)(?P<another>Rip)', value={'other': 'HD', 'another': 'Rip'}, + private_parent=True, children=True, validator={'__parent__': seps_surround}, validate_all=True) + + for value in ('Screener', 'Remux', '3D', 'PAL', 'SECAM', 'NTSC', 'XXX'): + rebulk.string(value, value=value) + + rebulk.string('HQ', value='High Quality', tags='uhdbluray-neighbor') + rebulk.string('HR', value='High Resolution') + rebulk.string('LD', value='Line Dubbed') + rebulk.string('MD', value='Mic Dubbed') + rebulk.string('mHD', 'HDLight', value='Micro HD') + rebulk.string('LDTV', value='Low Definition') + rebulk.string('HFR', value='High Frame Rate') + rebulk.string('HD', value='HD', validator=None, + tags=['streaming_service.prefix', 'streaming_service.suffix']) + rebulk.regex('Full-?HD', 'FHD', value='Full HD', validator=None, + tags=['streaming_service.prefix', 'streaming_service.suffix']) + rebulk.regex('Ultra-?(?:HD)?', 'UHD', value='Ultra HD', validator=None, + tags=['streaming_service.prefix', 'streaming_service.suffix']) + rebulk.regex('Upscaled?', value='Upscaled') + + for value in ('Complete', 'Classic', 'Bonus', 'Trailer', 'Retail', + 'Colorized', 'Internal'): + rebulk.string(value, value=value, tags=['has-neighbor', 'release-group-prefix']) + rebulk.regex('LiNE', value='Line Audio', tags=['has-neighbor-before', 'has-neighbor-after', 'release-group-prefix']) + rebulk.regex('Read-?NFO', value='Read NFO') + rebulk.string('CONVERT', value='Converted', tags='has-neighbor') + rebulk.string('DOCU', 'DOKU', value='Documentary', tags='has-neighbor') + rebulk.string('OM', value='Open Matte', tags='has-neighbor') + rebulk.string('STV', value='Straight to Video', tags='has-neighbor') + rebulk.string('OAR', value='Original Aspect Ratio', tags='has-neighbor') + rebulk.string('Complet', value='Complete', tags=['has-neighbor', 'release-group-prefix']) + + for coast in ('East', 'West'): + rebulk.regex(r'(?:Live-)?(?:Episode-)?' + coast + '-?(?:Coast-)?Feed', value=coast + ' Coast Feed') + + rebulk.string('VO', 'OV', value='Original Video', tags='has-neighbor') + rebulk.string('Ova', 'Oav', value='Original Animated Video') + + rebulk.regex('Scr(?:eener)?', value='Screener', validator=None, + tags=['other.validate.screener', 'source-prefix', 'source-suffix']) + rebulk.string('Mux', value='Mux', validator=seps_after, + tags=['other.validate.mux', 'video-codec-prefix', 'source-suffix']) + rebulk.string('HC', 'vost', value='Hardcoded Subtitles') + + rebulk.string('SDR', value='Standard Dynamic Range', tags='uhdbluray-neighbor') + rebulk.regex('HDR(?:10)?', value='HDR10', tags='uhdbluray-neighbor') + rebulk.regex('Dolby-?Vision', value='Dolby Vision', tags='uhdbluray-neighbor') + rebulk.regex('BT-?2020', value='BT.2020', tags='uhdbluray-neighbor') + + rebulk.string('Sample', value='Sample', tags=['at-end', 'not-a-release-group']) + rebulk.string('Proof', value='Proof', tags=['at-end', 'not-a-release-group']) + rebulk.string('Obfuscated', 'Scrambled', value='Obfuscated', tags=['at-end', 'not-a-release-group']) + rebulk.string('xpost', 'postbot', 'asrequested', value='Repost', tags='not-a-release-group') + + rebulk.rules(RenameAnotherToOther, ValidateHasNeighbor, ValidateHasNeighborAfter, ValidateHasNeighborBefore, + ValidateScreenerRule, ValidateMuxRule, ValidateHardcodedSubs, ValidateStreamingServiceNeighbor, + ValidateAtEnd, ProperCountRule) + + return rebulk + + +class ProperCountRule(Rule): + """ + Add proper_count property + """ + priority = POST_PROCESS + + consequence = AppendMatch + + properties = {'proper_count': [None]} + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + propers = matches.named('other', lambda match: match.value == 'Proper') + if propers: + raws = {} # Count distinct raw values + for proper in propers: + raws[raw_cleanup(proper.raw)] = proper + proper_count_match = copy.copy(propers[-1]) + proper_count_match.name = 'proper_count' + proper_count_match.value = len(raws) + return proper_count_match + + +class RenameAnotherToOther(Rule): + """ + Rename `another` properties to `other` + """ + priority = 32 + consequence = RenameMatch('other') + + def when(self, matches, context): + return matches.named('another') + + +class ValidateHasNeighbor(Rule): + """ + Validate tag has-neighbor + """ + consequence = RemoveMatch + priority = 64 + + def when(self, matches, context): + ret = [] + for to_check in matches.range(predicate=lambda match: 'has-neighbor' in match.tags): + previous_match = matches.previous(to_check, index=0) + previous_group = matches.markers.previous(to_check, lambda marker: marker.name == 'group', 0) + if previous_group and (not previous_match or previous_group.end > previous_match.end): + previous_match = previous_group + if previous_match and not matches.input_string[previous_match.end:to_check.start].strip(seps): + break + next_match = matches.next(to_check, index=0) + next_group = matches.markers.next(to_check, lambda marker: marker.name == 'group', 0) + if next_group and (not next_match or next_group.start < next_match.start): + next_match = next_group + if next_match and not matches.input_string[to_check.end:next_match.start].strip(seps): + break + ret.append(to_check) + return ret + + +class ValidateHasNeighborBefore(Rule): + """ + Validate tag has-neighbor-before that previous match exists. + """ + consequence = RemoveMatch + priority = 64 + + def when(self, matches, context): + ret = [] + for to_check in matches.range(predicate=lambda match: 'has-neighbor-before' in match.tags): + next_match = matches.next(to_check, index=0) + next_group = matches.markers.next(to_check, lambda marker: marker.name == 'group', 0) + if next_group and (not next_match or next_group.start < next_match.start): + next_match = next_group + if next_match and not matches.input_string[to_check.end:next_match.start].strip(seps): + break + ret.append(to_check) + return ret + + +class ValidateHasNeighborAfter(Rule): + """ + Validate tag has-neighbor-after that next match exists. + """ + consequence = RemoveMatch + priority = 64 + + def when(self, matches, context): + ret = [] + for to_check in matches.range(predicate=lambda match: 'has-neighbor-after' in match.tags): + previous_match = matches.previous(to_check, index=0) + previous_group = matches.markers.previous(to_check, lambda marker: marker.name == 'group', 0) + if previous_group and (not previous_match or previous_group.end > previous_match.end): + previous_match = previous_group + if previous_match and not matches.input_string[previous_match.end:to_check.start].strip(seps): + break + ret.append(to_check) + return ret + + +class ValidateScreenerRule(Rule): + """ + Validate tag other.validate.screener + """ + consequence = RemoveMatch + priority = 64 + + def when(self, matches, context): + ret = [] + for screener in matches.named('other', lambda match: 'other.validate.screener' in match.tags): + source_match = matches.previous(screener, lambda match: match.initiator.name == 'source', 0) + if not source_match or matches.input_string[source_match.end:screener.start].strip(seps): + ret.append(screener) + return ret + + +class ValidateMuxRule(Rule): + """ + Validate tag other.validate.mux + """ + consequence = RemoveMatch + priority = 64 + + def when(self, matches, context): + ret = [] + for mux in matches.named('other', lambda match: 'other.validate.mux' in match.tags): + source_match = matches.previous(mux, lambda match: match.initiator.name == 'source', 0) + if not source_match: + ret.append(mux) + return ret + + +class ValidateHardcodedSubs(Rule): + """Validate HC matches.""" + + priority = 32 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for hc_match in matches.named('other', predicate=lambda match: match.value == 'Hardcoded Subtitles'): + next_match = matches.next(hc_match, predicate=lambda match: match.name == 'subtitle_language', index=0) + if next_match and not matches.holes(hc_match.end, next_match.start, + predicate=lambda match: match.value.strip(seps)): + continue + + previous_match = matches.previous(hc_match, + predicate=lambda match: match.name == 'subtitle_language', index=0) + if previous_match and not matches.holes(previous_match.end, hc_match.start, + predicate=lambda match: match.value.strip(seps)): + continue + + to_remove.append(hc_match) + + return to_remove + + +class ValidateStreamingServiceNeighbor(Rule): + """Validate streaming service's neighbors.""" + + priority = 32 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for match in matches.named('other', + predicate=lambda m: (m.initiator.name != 'source' + and ('streaming_service.prefix' in m.tags + or 'streaming_service.suffix' in m.tags))): + match = match.initiator + if not seps_after(match): + if 'streaming_service.prefix' in match.tags: + next_match = matches.next(match, lambda m: m.name == 'streaming_service', 0) + if next_match and not matches.holes(match.end, next_match.start, + predicate=lambda m: m.value.strip(seps)): + continue + if match.children: + to_remove.extend(match.children) + to_remove.append(match) + + elif not seps_before(match): + if 'streaming_service.suffix' in match.tags: + previous_match = matches.previous(match, lambda m: m.name == 'streaming_service', 0) + if previous_match and not matches.holes(previous_match.end, match.start, + predicate=lambda m: m.value.strip(seps)): + continue + + if match.children: + to_remove.extend(match.children) + to_remove.append(match) + + return to_remove + + +class ValidateAtEnd(Rule): + """Validate other which should occur at the end of a filepart.""" + + priority = 32 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + for match in matches.range(filepart.start, filepart.end, + predicate=lambda m: m.name == 'other' and 'at-end' in m.tags): + if (matches.holes(match.end, filepart.end, predicate=lambda m: m.value.strip(seps)) or + matches.range(match.end, filepart.end, predicate=lambda m: m.name not in ( + 'other', 'container'))): + to_remove.append(match) + + return to_remove diff --git a/libs/guessit/rules/properties/part.py b/libs/guessit/rules/properties/part.py new file mode 100644 index 000000000..ec038b187 --- /dev/null +++ b/libs/guessit/rules/properties/part.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +part property +""" +from rebulk.remodule import re + +from rebulk import Rebulk +from ..common import dash +from ..common.pattern import is_disabled +from ..common.validators import seps_surround, int_coercable, compose +from ..common.numeral import numeral, parse_numeral +from ...reutils import build_or_pattern + + +def part(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'part')) + rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash], validator={'__parent__': seps_surround}) + + prefixes = config['prefixes'] + + def validate_roman(match): + """ + Validate a roman match if surrounded by separators + :param match: + :type match: + :return: + :rtype: + """ + if int_coercable(match.raw): + return True + return seps_surround(match) + + rebulk.regex(build_or_pattern(prefixes) + r'-?(?P<part>' + numeral + r')', + prefixes=prefixes, validate_all=True, private_parent=True, children=True, formatter=parse_numeral, + validator={'part': compose(validate_roman, lambda m: 0 < m.value < 100)}) + + return rebulk diff --git a/libs/guessit/rules/properties/release_group.py b/libs/guessit/rules/properties/release_group.py new file mode 100644 index 000000000..5144a9024 --- /dev/null +++ b/libs/guessit/rules/properties/release_group.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +release_group property +""" +import copy + +from rebulk import Rebulk, Rule, AppendMatch, RemoveMatch +from rebulk.match import Match + +from ..common import seps +from ..common.expected import build_expected_function +from ..common.comparators import marker_sorted +from ..common.formatters import cleanup +from ..common.pattern import is_disabled +from ..common.validators import int_coercable, seps_surround +from ..properties.title import TitleFromPosition + + +def release_group(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + forbidden_groupnames = config['forbidden_names'] + + groupname_ignore_seps = config['ignored_seps'] + groupname_seps = ''.join([c for c in seps if c not in groupname_ignore_seps]) + + def clean_groupname(string): + """ + Removes and strip separators from input_string + :param string: + :type string: + :return: + :rtype: + """ + string = string.strip(groupname_seps) + if not (string.endswith(tuple(groupname_ignore_seps)) and string.startswith(tuple(groupname_ignore_seps))) \ + and not any(i in string.strip(groupname_ignore_seps) for i in groupname_ignore_seps): + string = string.strip(groupname_ignore_seps) + for forbidden in forbidden_groupnames: + if string.lower().startswith(forbidden) and string[len(forbidden):len(forbidden) + 1] in seps: + string = string[len(forbidden):] + string = string.strip(groupname_seps) + if string.lower().endswith(forbidden) and string[-len(forbidden) - 1:-len(forbidden)] in seps: + string = string[:len(forbidden)] + string = string.strip(groupname_seps) + return string + + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'release_group')) + + expected_group = build_expected_function('expected_group') + + rebulk.functional(expected_group, name='release_group', tags=['expected'], + validator=seps_surround, + conflict_solver=lambda match, other: other, + disabled=lambda context: not context.get('expected_group')) + + return rebulk.rules( + DashSeparatedReleaseGroup(clean_groupname), + SceneReleaseGroup(clean_groupname), + AnimeReleaseGroup + ) + + +_scene_previous_names = ('video_codec', 'source', 'video_api', 'audio_codec', 'audio_profile', 'video_profile', + 'audio_channels', 'screen_size', 'other', 'container', 'language', 'subtitle_language', + 'subtitle_language.suffix', 'subtitle_language.prefix', 'language.suffix') + +_scene_previous_tags = ('release-group-prefix', ) + + +class DashSeparatedReleaseGroup(Rule): + """ + Detect dash separated release groups that might appear at the end or at the beginning of a release name. + + Series.S01E02.Pilot.DVDRip.x264-CS.mkv + release_group: CS + abc-the.title.name.1983.1080p.bluray.x264.mkv + release_group: abc + + At the end: Release groups should be dash-separated and shouldn't contain spaces nor + appear in a group with other matches. The preceding matches should be separated by dot. + If a release group is found, the conflicting matches are removed. + + At the beginning: Release groups should be dash-separated and shouldn't contain spaces nor appear in a group. + It should be followed by a hole with dot-separated words. + Detection only happens if no matches exist at the beginning. + """ + consequence = [RemoveMatch, AppendMatch] + + def __init__(self, value_formatter): + """Default constructor.""" + super(DashSeparatedReleaseGroup, self).__init__() + self.value_formatter = value_formatter + + @classmethod + def is_valid(cls, matches, candidate, start, end, at_end): # pylint:disable=inconsistent-return-statements + """ + Whether a candidate is a valid release group. + """ + if not at_end: + if len(candidate.value) <= 1: + return False + + if matches.markers.at_match(candidate, predicate=lambda m: m.name == 'group'): + return False + + first_hole = matches.holes(candidate.end, end, predicate=lambda m: m.start == candidate.end, index=0) + if not first_hole: + return False + + raw_value = first_hole.raw + return raw_value[0] == '-' and '-' not in raw_value[1:] and '.' in raw_value and ' ' not in raw_value + + group = matches.markers.at_match(candidate, predicate=lambda m: m.name == 'group', index=0) + if group and matches.at_match(group, predicate=lambda m: not m.private and m.span != candidate.span): + return False + + count = 0 + match = candidate + while match: + current = matches.range(start, match.start, index=-1, predicate=lambda m: not m.private) + if not current: + break + + separator = match.input_string[current.end:match.start] + if not separator and match.raw[0] == '-': + separator = '-' + + match = current + + if count == 0: + if separator != '-': + break + + count += 1 + continue + + if separator == '.': + return True + + def detect(self, matches, start, end, at_end): # pylint:disable=inconsistent-return-statements + """ + Detect release group at the end or at the beginning of a filepart. + """ + candidate = None + if at_end: + container = matches.ending(end, lambda m: m.name == 'container', index=0) + if container: + end = container.start + + candidate = matches.ending(end, index=0, predicate=( + lambda m: not m.private and not ( + m.name == 'other' and 'not-a-release-group' in m.tags + ) and '-' not in m.raw and m.raw.strip() == m.raw)) + + if not candidate: + if at_end: + candidate = matches.holes(start, end, seps=seps, index=-1, + predicate=lambda m: m.end == end and m.raw.strip(seps) and m.raw[0] == '-') + else: + candidate = matches.holes(start, end, seps=seps, index=0, + predicate=lambda m: m.start == start and m.raw.strip(seps)) + + if candidate and self.is_valid(matches, candidate, start, end, at_end): + return candidate + + def when(self, matches, context): # pylint:disable=inconsistent-return-statements + if matches.named('release_group'): + return + + to_remove = [] + to_append = [] + for filepart in matches.markers.named('path'): + candidate = self.detect(matches, filepart.start, filepart.end, True) + if candidate: + to_remove.extend(matches.at_match(candidate)) + else: + candidate = self.detect(matches, filepart.start, filepart.end, False) + + if candidate: + releasegroup = Match(candidate.start, candidate.end, name='release_group', + formatter=self.value_formatter, input_string=candidate.input_string) + + to_append.append(releasegroup) + return to_remove, to_append + + +class SceneReleaseGroup(Rule): + """ + Add release_group match in existing matches (scene format). + + Something.XViD-ReleaseGroup.mkv + """ + dependency = [TitleFromPosition] + consequence = AppendMatch + + properties = {'release_group': [None]} + + def __init__(self, value_formatter): + """Default constructor.""" + super(SceneReleaseGroup, self).__init__() + self.value_formatter = value_formatter + + def when(self, matches, context): # pylint:disable=too-many-locals + # If a release_group is found before, ignore this kind of release_group rule. + + ret = [] + + for filepart in marker_sorted(matches.markers.named('path'), matches): + # pylint:disable=cell-var-from-loop + start, end = filepart.span + if matches.named('release_group', predicate=lambda m: m.start >= start and m.end <= end): + continue + + titles = matches.named('title', predicate=lambda m: m.start >= start and m.end <= end) + + def keep_only_first_title(match): + """ + Keep only first title from this filepart, as other ones are most likely release group. + + :param match: + :type match: + :return: + :rtype: + """ + return match in titles[1:] + + last_hole = matches.holes(start, end + 1, formatter=self.value_formatter, + ignore=keep_only_first_title, + predicate=lambda hole: cleanup(hole.value), index=-1) + + if last_hole: + def previous_match_filter(match): + """ + Filter to apply to find previous match + + :param match: + :type match: + :return: + :rtype: + """ + + if match.start < filepart.start: + return False + return not match.private or match.name in _scene_previous_names + + previous_match = matches.previous(last_hole, + previous_match_filter, + index=0) + if previous_match and (previous_match.name in _scene_previous_names or + any(tag in previous_match.tags for tag in _scene_previous_tags)) and \ + not matches.input_string[previous_match.end:last_hole.start].strip(seps) \ + and not int_coercable(last_hole.value.strip(seps)): + + last_hole.name = 'release_group' + last_hole.tags = ['scene'] + + # if hole is inside a group marker with same value, remove [](){} ... + group = matches.markers.at_match(last_hole, lambda marker: marker.name == 'group', 0) + if group: + group.formatter = self.value_formatter + if group.value == last_hole.value: + last_hole.start = group.start + 1 + last_hole.end = group.end - 1 + last_hole.tags = ['anime'] + + ignored_matches = matches.range(last_hole.start, last_hole.end, keep_only_first_title) + + for ignored_match in ignored_matches: + matches.remove(ignored_match) + + ret.append(last_hole) + return ret + + +class AnimeReleaseGroup(Rule): + """ + Add release_group match in existing matches (anime format) + ...[ReleaseGroup] Something.mkv + """ + dependency = [SceneReleaseGroup, TitleFromPosition] + consequence = [RemoveMatch, AppendMatch] + + properties = {'release_group': [None]} + + def when(self, matches, context): + to_remove = [] + to_append = [] + + # If a release_group is found before, ignore this kind of release_group rule. + if matches.named('release_group'): + return to_remove, to_append + + if not matches.named('episode') and not matches.named('season') and matches.named('release_group'): + # This doesn't seems to be an anime, and we already found another release_group. + return to_remove, to_append + + for filepart in marker_sorted(matches.markers.named('path'), matches): + + # pylint:disable=bad-continuation + empty_group = matches.markers.range(filepart.start, + filepart.end, + lambda marker: (marker.name == 'group' + and not matches.range(marker.start, marker.end, + lambda m: + 'weak-language' not in m.tags) + and marker.value.strip(seps) + and not int_coercable(marker.value.strip(seps))), 0) + + if empty_group: + group = copy.copy(empty_group) + group.marker = False + group.raw_start += 1 + group.raw_end -= 1 + group.tags = ['anime'] + group.name = 'release_group' + to_append.append(group) + to_remove.extend(matches.range(empty_group.start, empty_group.end, + lambda m: 'weak-language' in m.tags)) + return to_remove, to_append diff --git a/libs/guessit/rules/properties/screen_size.py b/libs/guessit/rules/properties/screen_size.py new file mode 100644 index 000000000..83a797c1f --- /dev/null +++ b/libs/guessit/rules/properties/screen_size.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +screen_size property +""" +from rebulk.match import Match +from rebulk.remodule import re + +from rebulk import Rebulk, Rule, RemoveMatch, AppendMatch + +from ..common.pattern import is_disabled +from ..common.quantity import FrameRate +from ..common.validators import seps_surround +from ..common import dash, seps +from ...reutils import build_or_pattern + + +def screen_size(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + interlaced = frozenset({res for res in config['interlaced']}) + progressive = frozenset({res for res in config['progressive']}) + frame_rates = [re.escape(rate) for rate in config['frame_rates']] + min_ar = config['min_ar'] + max_ar = config['max_ar'] + + rebulk = Rebulk() + rebulk = rebulk.string_defaults(ignore_case=True).regex_defaults(flags=re.IGNORECASE) + + rebulk.defaults(name='screen_size', validator=seps_surround, abbreviations=[dash], + disabled=lambda context: is_disabled(context, 'screen_size')) + + frame_rate_pattern = build_or_pattern(frame_rates, name='frame_rate') + interlaced_pattern = build_or_pattern(interlaced, name='height') + progressive_pattern = build_or_pattern(progressive, name='height') + + res_pattern = r'(?:(?P<width>\d{3,4})(?:x|\*))?' + rebulk.regex(res_pattern + interlaced_pattern + r'(?P<scan_type>i)' + frame_rate_pattern + '?') + rebulk.regex(res_pattern + progressive_pattern + r'(?P<scan_type>p)' + frame_rate_pattern + '?') + rebulk.regex(res_pattern + progressive_pattern + r'(?P<scan_type>p)?(?:hd)') + rebulk.regex(res_pattern + progressive_pattern + r'(?P<scan_type>p)?x?') + rebulk.string('4k', value='2160p') + rebulk.regex(r'(?P<width>\d{3,4})-?(?:x|\*)-?(?P<height>\d{3,4})', + conflict_solver=lambda match, other: '__default__' if other.name == 'screen_size' else other) + + rebulk.regex(frame_rate_pattern + '(p|fps)', name='frame_rate', + formatter=FrameRate.fromstring, disabled=lambda context: is_disabled(context, 'frame_rate')) + + rebulk.rules(PostProcessScreenSize(progressive, min_ar, max_ar), ScreenSizeOnlyOne, ResolveScreenSizeConflicts) + + return rebulk + + +class PostProcessScreenSize(Rule): + """ + Process the screen size calculating the aspect ratio if available. + + Convert to a standard notation (720p, 1080p, etc) when it's a standard resolution and + aspect ratio is valid or not available. + + It also creates an aspect_ratio match when available. + """ + consequence = AppendMatch + + def __init__(self, standard_heights, min_ar, max_ar): + super(PostProcessScreenSize, self).__init__() + self.standard_heights = standard_heights + self.min_ar = min_ar + self.max_ar = max_ar + + def when(self, matches, context): + to_append = [] + for match in matches.named('screen_size'): + if not is_disabled(context, 'frame_rate'): + for frame_rate in match.children.named('frame_rate'): + frame_rate.formatter = FrameRate.fromstring + to_append.append(frame_rate) + + values = match.children.to_dict() + if 'height' not in values: + continue + + scan_type = (values.get('scan_type') or 'p').lower() + height = values['height'] + if 'width' not in values: + match.value = '{0}{1}'.format(height, scan_type) + continue + + width = values['width'] + calculated_ar = float(width) / float(height) + + aspect_ratio = Match(match.start, match.end, input_string=match.input_string, + name='aspect_ratio', value=round(calculated_ar, 3)) + + if not is_disabled(context, 'aspect_ratio'): + to_append.append(aspect_ratio) + + if height in self.standard_heights and self.min_ar < calculated_ar < self.max_ar: + match.value = '{0}{1}'.format(height, scan_type) + else: + match.value = '{0}x{1}'.format(width, height) + + return to_append + + +class ScreenSizeOnlyOne(Rule): + """ + Keep a single screen_size per filepath part. + """ + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + screensize = list(reversed(matches.range(filepart.start, filepart.end, + lambda match: match.name == 'screen_size'))) + if len(screensize) > 1 and len(set((match.value for match in screensize))) > 1: + to_remove.extend(screensize[1:]) + + return to_remove + + +class ResolveScreenSizeConflicts(Rule): + """ + Resolve screen_size conflicts with season and episode matches. + """ + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for filepart in matches.markers.named('path'): + screensize = matches.range(filepart.start, filepart.end, lambda match: match.name == 'screen_size', 0) + if not screensize: + continue + + conflicts = matches.conflicting(screensize, lambda match: match.name in ('season', 'episode')) + if not conflicts: + continue + + has_neighbor = False + video_profile = matches.range(screensize.end, filepart.end, lambda match: match.name == 'video_profile', 0) + if video_profile and not matches.holes(screensize.end, video_profile.start, + predicate=lambda h: h.value and h.value.strip(seps)): + to_remove.extend(conflicts) + has_neighbor = True + + previous = matches.previous(screensize, index=0, predicate=( + lambda m: m.name in ('date', 'source', 'other', 'streaming_service'))) + if previous and not matches.holes(previous.end, screensize.start, + predicate=lambda h: h.value and h.value.strip(seps)): + to_remove.extend(conflicts) + has_neighbor = True + + if not has_neighbor: + to_remove.append(screensize) + + return to_remove diff --git a/libs/guessit/rules/properties/size.py b/libs/guessit/rules/properties/size.py new file mode 100644 index 000000000..c61580c04 --- /dev/null +++ b/libs/guessit/rules/properties/size.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +size property +""" +import re + +from rebulk import Rebulk + +from ..common import dash +from ..common.quantity import Size +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def size(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'size')) + rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]) + rebulk.defaults(name='size', validator=seps_surround) + rebulk.regex(r'\d+-?[mgt]b', r'\d+\.\d+-?[mgt]b', formatter=Size.fromstring, tags=['release-group-prefix']) + + return rebulk diff --git a/libs/guessit/rules/properties/source.py b/libs/guessit/rules/properties/source.py new file mode 100644 index 000000000..ae9a7b03a --- /dev/null +++ b/libs/guessit/rules/properties/source.py @@ -0,0 +1,201 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +source property +""" +import copy + +from rebulk.remodule import re + +from rebulk import AppendMatch, Rebulk, RemoveMatch, Rule + +from .audio_codec import HqConflictRule +from ..common import dash, seps +from ..common.pattern import is_disabled +from ..common.validators import seps_before, seps_after + + +def source(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'source')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash], private_parent=True, children=True) + rebulk.defaults(name='source', tags=['video-codec-prefix', 'streaming_service.suffix']) + + rip_prefix = '(?P<other>Rip)-?' + rip_suffix = '-?(?P<other>Rip)' + rip_optional_suffix = '(?:' + rip_suffix + ')?' + + def build_source_pattern(*patterns, **kwargs): + """Helper pattern to build source pattern.""" + prefix_format = kwargs.get('prefix') or '' + suffix_format = kwargs.get('suffix') or '' + + string_format = prefix_format + '({0})' + suffix_format + return [string_format.format(pattern) for pattern in patterns] + + def demote_other(match, other): # pylint: disable=unused-argument + """Default conflict solver with 'other' property.""" + return other if other.name == 'other' else '__default__' + + rebulk.regex(*build_source_pattern('VHS', suffix=rip_optional_suffix), + value={'source': 'VHS', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('CAM', suffix=rip_optional_suffix), + value={'source': 'Camera', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('HD-?CAM', suffix=rip_optional_suffix), + value={'source': 'HD Camera', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TELESYNC', 'TS', suffix=rip_optional_suffix), + value={'source': 'Telesync', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('HD-?TELESYNC', 'HD-?TS', suffix=rip_optional_suffix), + value={'source': 'HD Telesync', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('WORKPRINT', 'WP'), value='Workprint') + rebulk.regex(*build_source_pattern('TELECINE', 'TC', suffix=rip_optional_suffix), + value={'source': 'Telecine', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('HD-?TELECINE', 'HD-?TC', suffix=rip_optional_suffix), + value={'source': 'HD Telecine', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('PPV', suffix=rip_optional_suffix), + value={'source': 'Pay-per-view', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('SD-?TV', suffix=rip_optional_suffix), + value={'source': 'TV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TV', suffix=rip_suffix), # TV is too common to allow matching + value={'source': 'TV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TV', 'SD-?TV', prefix=rip_prefix), + value={'source': 'TV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TV-?(?=Dub)'), value='TV') + rebulk.regex(*build_source_pattern('DVB', 'PD-?TV', suffix=rip_optional_suffix), + value={'source': 'Digital TV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('DVD', suffix=rip_optional_suffix), + value={'source': 'DVD', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('DM', suffix=rip_optional_suffix), + value={'source': 'Digital Master', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('VIDEO-?TS', 'DVD-?R(?:$|(?!E))', # 'DVD-?R(?:$|^E)' => DVD-Real ... + 'DVD-?9', 'DVD-?5'), value='DVD') + + rebulk.regex(*build_source_pattern('HD-?TV', suffix=rip_optional_suffix), conflict_solver=demote_other, + value={'source': 'HDTV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TV-?HD', suffix=rip_suffix), conflict_solver=demote_other, + value={'source': 'HDTV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('TV', suffix='-?(?P<other>Rip-?HD)'), conflict_solver=demote_other, + value={'source': 'HDTV', 'other': 'Rip'}) + + rebulk.regex(*build_source_pattern('VOD', suffix=rip_optional_suffix), + value={'source': 'Video on Demand', 'other': 'Rip'}) + + rebulk.regex(*build_source_pattern('WEB', 'WEB-?DL', suffix=rip_suffix), + value={'source': 'Web', 'other': 'Rip'}) + # WEBCap is a synonym to WEBRip, mostly used by non english + rebulk.regex(*build_source_pattern('WEB-?(?P<another>Cap)', suffix=rip_optional_suffix), + value={'source': 'Web', 'other': 'Rip', 'another': 'Rip'}) + rebulk.regex(*build_source_pattern('WEB-?DL', 'WEB-?U?HD', 'WEB', 'DL-?WEB', 'DL(?=-?Mux)'), + value={'source': 'Web'}) + + rebulk.regex(*build_source_pattern('HD-?DVD', suffix=rip_optional_suffix), + value={'source': 'HD-DVD', 'other': 'Rip'}) + + rebulk.regex(*build_source_pattern('Blu-?ray', 'BD', 'BD[59]', 'BD25', 'BD50', suffix=rip_optional_suffix), + value={'source': 'Blu-ray', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('(?P<another>BR)-?(?=Scr(?:eener)?)', '(?P<another>BR)-?(?=Mux)'), # BRRip + value={'source': 'Blu-ray', 'another': 'Reencoded'}) + rebulk.regex(*build_source_pattern('(?P<another>BR)', suffix=rip_suffix), # BRRip + value={'source': 'Blu-ray', 'other': 'Rip', 'another': 'Reencoded'}) + + rebulk.regex(*build_source_pattern('Ultra-?Blu-?ray', 'Blu-?ray-?Ultra'), value='Ultra HD Blu-ray') + + rebulk.regex(*build_source_pattern('AHDTV'), value='Analog HDTV') + rebulk.regex(*build_source_pattern('UHD-?TV', suffix=rip_optional_suffix), conflict_solver=demote_other, + value={'source': 'Ultra HDTV', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('UHD', suffix=rip_suffix), conflict_solver=demote_other, + value={'source': 'Ultra HDTV', 'other': 'Rip'}) + + rebulk.regex(*build_source_pattern('DSR', 'DTH', suffix=rip_optional_suffix), + value={'source': 'Satellite', 'other': 'Rip'}) + rebulk.regex(*build_source_pattern('DSR?', 'SAT', suffix=rip_suffix), + value={'source': 'Satellite', 'other': 'Rip'}) + + rebulk.rules(ValidateSource, UltraHdBlurayRule) + + return rebulk + + +class UltraHdBlurayRule(Rule): + """ + Replace other:Ultra HD and source:Blu-ray with source:Ultra HD Blu-ray + """ + dependency = HqConflictRule + consequence = [RemoveMatch, AppendMatch] + + @classmethod + def find_ultrahd(cls, matches, start, end, index): + """Find Ultra HD match.""" + return matches.range(start, end, index=index, predicate=( + lambda m: not m.private and m.name == 'other' and m.value == 'Ultra HD' + )) + + @classmethod + def validate_range(cls, matches, start, end): + """Validate no holes or invalid matches exist in the specified range.""" + return ( + not matches.holes(start, end, predicate=lambda m: m.value.strip(seps)) and + not matches.range(start, end, predicate=( + lambda m: not m.private and ( + m.name not in ('screen_size', 'color_depth') and ( + m.name != 'other' or 'uhdbluray-neighbor' not in m.tags)))) + ) + + def when(self, matches, context): + to_remove = [] + to_append = [] + for filepart in matches.markers.named('path'): + for match in matches.range(filepart.start, filepart.end, predicate=( + lambda m: not m.private and m.name == 'source' and m.value == 'Blu-ray')): + other = self.find_ultrahd(matches, filepart.start, match.start, -1) + if not other or not self.validate_range(matches, other.end, match.start): + other = self.find_ultrahd(matches, match.end, filepart.end, 0) + if not other or not self.validate_range(matches, match.end, other.start): + if not matches.range(filepart.start, filepart.end, predicate=( + lambda m: m.name == 'screen_size' and m.value == '2160p')): + continue + + if other: + other.private = True + + new_source = copy.copy(match) + new_source.value = 'Ultra HD Blu-ray' + to_remove.append(match) + to_append.append(new_source) + + return to_remove, to_append + + +class ValidateSource(Rule): + """ + Validate source with screener property, with video_codec property or separated + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + ret = [] + for match in matches.named('source'): + match = match.initiator + if not seps_before(match) and \ + not matches.range(match.start - 1, match.start - 2, + lambda m: 'source-prefix' in m.tags): + if match.children: + ret.extend(match.children) + ret.append(match) + continue + if not seps_after(match) and \ + not matches.range(match.end, match.end + 1, + lambda m: 'source-suffix' in m.tags): + if match.children: + ret.extend(match.children) + ret.append(match) + continue + return ret diff --git a/libs/guessit/rules/properties/streaming_service.py b/libs/guessit/rules/properties/streaming_service.py new file mode 100644 index 000000000..1302befb0 --- /dev/null +++ b/libs/guessit/rules/properties/streaming_service.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +streaming_service property +""" +import re + +from rebulk import Rebulk +from rebulk.rules import Rule, RemoveMatch + +from ..common.pattern import is_disabled +from ...rules.common import seps, dash +from ...rules.common.validators import seps_before, seps_after + + +def streaming_service(config): # pylint: disable=too-many-statements,unused-argument + """Streaming service property. + + :param config: rule configuration + :type config: dict + :return: + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'streaming_service')) + rebulk = rebulk.string_defaults(ignore_case=True).regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]) + rebulk.defaults(name='streaming_service', tags=['source-prefix']) + + rebulk.string('AE', 'A&E', value='A&E') + rebulk.string('AMBC', value='ABC') + rebulk.string('AUBC', value='ABC Australia') + rebulk.string('AJAZ', value='Al Jazeera English') + rebulk.string('AMC', value='AMC') + rebulk.string('AMZN', 'Amazon', value='Amazon Prime') + rebulk.regex('Amazon-?Prime', value='Amazon Prime') + rebulk.string('AS', value='Adult Swim') + rebulk.regex('Adult-?Swim', value='Adult Swim') + rebulk.string('ATK', value="America's Test Kitchen") + rebulk.string('ANPL', value='Animal Planet') + rebulk.string('ANLB', value='AnimeLab') + rebulk.string('AOL', value='AOL') + rebulk.string('ARD', value='ARD') + rebulk.string('iP', value='BBC iPlayer') + rebulk.regex('BBC-?iPlayer', value='BBC iPlayer') + rebulk.string('BRAV', value='BravoTV') + rebulk.string('CNLP', value='Canal+') + rebulk.string('CN', value='Cartoon Network') + rebulk.string('CBC', value='CBC') + rebulk.string('CBS', value='CBS') + rebulk.string('CNBC', value='CNBC') + rebulk.string('CC', value='Comedy Central') + rebulk.string('4OD', value='Channel 4') + rebulk.string('CHGD', value='CHRGD') + rebulk.string('CMAX', value='Cinemax') + rebulk.string('CMT', value='Country Music Television') + rebulk.regex('Comedy-?Central', value='Comedy Central') + rebulk.string('CCGC', value='Comedians in Cars Getting Coffee') + rebulk.string('CR', value='Crunchy Roll') + rebulk.string('CRKL', value='Crackle') + rebulk.regex('Crunchy-?Roll', value='Crunchy Roll') + rebulk.string('CSPN', value='CSpan') + rebulk.string('CTV', value='CTV') + rebulk.string('CUR', value='CuriosityStream') + rebulk.string('CWS', value='CWSeed') + rebulk.string('DSKI', value='Daisuki') + rebulk.string('DHF', value='Deadhouse Films') + rebulk.string('DDY', value='Digiturk Diledigin Yerde') + rebulk.string('DISC', 'Discovery', value='Discovery') + rebulk.string('DSNY', 'Disney', value='Disney') + rebulk.string('DIY', value='DIY Network') + rebulk.string('DOCC', value='Doc Club') + rebulk.string('DPLY', value='DPlay') + rebulk.string('ETV', value='E!') + rebulk.string('EPIX', value='ePix') + rebulk.string('ETTV', value='El Trece') + rebulk.string('ESPN', value='ESPN') + rebulk.string('ESQ', value='Esquire') + rebulk.string('FAM', value='Family') + rebulk.string('FJR', value='Family Jr') + rebulk.string('FOOD', value='Food Network') + rebulk.string('FOX', value='Fox') + rebulk.string('FREE', value='Freeform') + rebulk.string('FYI', value='FYI Network') + rebulk.string('GLBL', value='Global') + rebulk.string('GLOB', value='GloboSat Play') + rebulk.string('HLMK', value='Hallmark') + rebulk.string('HBO', value='HBO Go') + rebulk.regex('HBO-?Go', value='HBO Go') + rebulk.string('HGTV', value='HGTV') + rebulk.string('HIST', 'History', value='History') + rebulk.string('HULU', value='Hulu') + rebulk.string('ID', value='Investigation Discovery') + rebulk.string('IFC', value='IFC') + rebulk.string('iTunes', 'iT', value='iTunes') + rebulk.string('ITV', value='ITV') + rebulk.string('KNOW', value='Knowledge Network') + rebulk.string('LIFE', value='Lifetime') + rebulk.string('MTOD', value='Motor Trend OnDemand') + rebulk.string('MNBC', value='MSNBC') + rebulk.string('MTV', value='MTV') + rebulk.string('NATG', value='National Geographic') + rebulk.regex('National-?Geographic', value='National Geographic') + rebulk.string('NBA', value='NBA TV') + rebulk.regex('NBA-?TV', value='NBA TV') + rebulk.string('NBC', value='NBC') + rebulk.string('NF', 'Netflix', value='Netflix') + rebulk.string('NFL', value='NFL') + rebulk.string('NFLN', value='NFL Now') + rebulk.string('GC', value='NHL GameCenter') + rebulk.string('NICK', 'Nickelodeon', value='Nickelodeon') + rebulk.string('NRK', value='Norsk Rikskringkasting') + rebulk.string('PBS', value='PBS') + rebulk.string('PBSK', value='PBS Kids') + rebulk.string('PSN', value='Playstation Network') + rebulk.string('PLUZ', value='Pluzz') + rebulk.string('RTE', value='RTE One') + rebulk.string('SBS', value='SBS (AU)') + rebulk.string('SESO', 'SeeSo', value='SeeSo') + rebulk.string('SHMI', value='Shomi') + rebulk.string('SPIK', value='Spike') + rebulk.string('SPKE', value='Spike TV') + rebulk.regex('Spike-?TV', value='Spike TV') + rebulk.string('SNET', value='Sportsnet') + rebulk.string('SPRT', value='Sprout') + rebulk.string('STAN', value='Stan') + rebulk.string('STZ', value='Starz') + rebulk.string('SVT', value='Sveriges Television') + rebulk.string('SWER', value='SwearNet') + rebulk.string('SYFY', value='Syfy') + rebulk.string('TBS', value='TBS') + rebulk.string('TFOU', value='TFou') + rebulk.string('CW', value='The CW') + rebulk.regex('The-?CW', value='The CW') + rebulk.string('TLC', value='TLC') + rebulk.string('TUBI', value='TubiTV') + rebulk.string('TV3', value='TV3 Ireland') + rebulk.string('TV4', value='TV4 Sweeden') + rebulk.string('TVL', value='TV Land') + rebulk.regex('TV-?Land', value='TV Land') + rebulk.string('UFC', value='UFC') + rebulk.string('UKTV', value='UKTV') + rebulk.string('UNIV', value='Univision') + rebulk.string('USAN', value='USA Network') + rebulk.string('VLCT', value='Velocity') + rebulk.string('VH1', value='VH1') + rebulk.string('VICE', value='Viceland') + rebulk.string('VMEO', value='Vimeo') + rebulk.string('VRV', value='VRV') + rebulk.string('WNET', value='W Network') + rebulk.string('WME', value='WatchMe') + rebulk.string('WWEN', value='WWE Network') + rebulk.string('XBOX', value='Xbox Video') + rebulk.string('YHOO', value='Yahoo') + rebulk.string('RED', value='YouTube Red') + rebulk.string('ZDF', value='ZDF') + + rebulk.rules(ValidateStreamingService) + + return rebulk + + +class ValidateStreamingService(Rule): + """Validate streaming service matches.""" + + priority = 32 + consequence = RemoveMatch + + def when(self, matches, context): + """Streaming service is always before source. + + :param matches: + :type matches: rebulk.match.Matches + :param context: + :type context: dict + :return: + """ + to_remove = [] + for service in matches.named('streaming_service'): + next_match = matches.next(service, lambda match: 'streaming_service.suffix' in match.tags, 0) + previous_match = matches.previous(service, lambda match: 'streaming_service.prefix' in match.tags, 0) + has_other = service.initiator and service.initiator.children.named('other') + + if not has_other: + if (not next_match or + matches.holes(service.end, next_match.start, + predicate=lambda match: match.value.strip(seps)) or + not seps_before(service)): + if (not previous_match or + matches.holes(previous_match.end, service.start, + predicate=lambda match: match.value.strip(seps)) or + not seps_after(service)): + to_remove.append(service) + continue + + if service.value == 'Comedy Central': + # Current match is a valid streaming service, removing invalid Criterion Collection (CC) matches + to_remove.extend(matches.named('edition', predicate=lambda match: match.value == 'Criterion')) + + return to_remove diff --git a/libs/guessit/rules/properties/title.py b/libs/guessit/rules/properties/title.py new file mode 100644 index 000000000..798df1e2a --- /dev/null +++ b/libs/guessit/rules/properties/title.py @@ -0,0 +1,332 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +title property +""" + +from rebulk import Rebulk, Rule, AppendMatch, RemoveMatch, AppendTags +from rebulk.formatters import formatters + +from .film import FilmTitleRule +from .language import SubtitlePrefixLanguageRule, SubtitleSuffixLanguageRule, SubtitleExtensionRule +from ..common import seps, title_seps +from ..common.comparators import marker_sorted +from ..common.expected import build_expected_function +from ..common.formatters import cleanup, reorder_title +from ..common.pattern import is_disabled +from ..common.validators import seps_surround + + +def title(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'title')) + rebulk.rules(TitleFromPosition, PreferTitleWithYear) + + expected_title = build_expected_function('expected_title') + + rebulk.functional(expected_title, name='title', tags=['expected', 'title'], + validator=seps_surround, + formatter=formatters(cleanup, reorder_title), + conflict_solver=lambda match, other: other, + disabled=lambda context: not context.get('expected_title')) + + return rebulk + + +class TitleBaseRule(Rule): + """ + Add title match in existing matches + """ + # pylint:disable=no-self-use,unused-argument + consequence = [AppendMatch, RemoveMatch] + + def __init__(self, match_name, match_tags=None, alternative_match_name=None): + super(TitleBaseRule, self).__init__() + self.match_name = match_name + self.match_tags = match_tags + self.alternative_match_name = alternative_match_name + + def hole_filter(self, hole, matches): + """ + Filter holes for titles. + :param hole: + :type hole: + :param matches: + :type matches: + :return: + :rtype: + """ + return True + + def filepart_filter(self, filepart, matches): + """ + Filter filepart for titles. + :param filepart: + :type filepart: + :param matches: + :type matches: + :return: + :rtype: + """ + return True + + def holes_process(self, holes, matches): + """ + process holes + :param holes: + :type holes: + :param matches: + :type matches: + :return: + :rtype: + """ + cropped_holes = [] + for hole in holes: + group_markers = matches.markers.named('group') + cropped_holes.extend(hole.crop(group_markers)) + return cropped_holes + + def is_ignored(self, match): + """ + Ignore matches when scanning for title (hole). + + Full word language and countries won't be ignored if they are uppercase. + """ + return not (len(match) > 3 and match.raw.isupper()) and match.name in ('language', 'country', 'episode_details') + + def should_keep(self, match, to_keep, matches, filepart, hole, starting): + """ + Check if this match should be accepted when ending or starting a hole. + :param match: + :type match: + :param to_keep: + :type to_keep: list[Match] + :param matches: + :type matches: Matches + :param hole: the filepart match + :type hole: Match + :param hole: the hole match + :type hole: Match + :param starting: true if match is starting the hole + :type starting: bool + :return: + :rtype: + """ + if match.name in ('language', 'country'): + # Keep language if exactly matching the hole. + if len(hole.value) == len(match.raw): + return True + + # Keep language if other languages exists in the filepart. + outside_matches = filepart.crop(hole) + other_languages = [] + for outside in outside_matches: + other_languages.extend(matches.range(outside.start, outside.end, + lambda c_match: c_match.name == match.name and + c_match not in to_keep)) + + if not other_languages and (not starting or len(match.raw) <= 3): + return True + + return False + + def should_remove(self, match, matches, filepart, hole, context): + """ + Check if this match should be removed after beeing ignored. + :param match: + :param matches: + :param filepart: + :param hole: + :return: + """ + if context.get('type') == 'episode' and match.name == 'episode_details': + return match.start >= hole.start and match.end <= hole.end + return True + + def check_titles_in_filepart(self, filepart, matches, context): # pylint:disable=inconsistent-return-statements + """ + Find title in filepart (ignoring language) + """ + # pylint:disable=too-many-locals,too-many-branches,too-many-statements + start, end = filepart.span + + holes = matches.holes(start, end + 1, formatter=formatters(cleanup, reorder_title), + ignore=self.is_ignored, + predicate=lambda m: m.value) + + holes = self.holes_process(holes, matches) + + for hole in holes: + if not hole or (self.hole_filter and not self.hole_filter(hole, matches)): + continue + + to_remove = [] + to_keep = [] + + ignored_matches = matches.range(hole.start, hole.end, self.is_ignored) + + if ignored_matches: + for ignored_match in reversed(ignored_matches): + # pylint:disable=undefined-loop-variable, cell-var-from-loop + trailing = matches.chain_before(hole.end, seps, predicate=lambda m: m == ignored_match) + if trailing: + should_keep = self.should_keep(ignored_match, to_keep, matches, filepart, hole, False) + if should_keep: + # pylint:disable=unpacking-non-sequence + try: + append, crop = should_keep + except TypeError: + append, crop = should_keep, should_keep + if append: + to_keep.append(ignored_match) + if crop: + hole.end = ignored_match.start + + for ignored_match in ignored_matches: + if ignored_match not in to_keep: + starting = matches.chain_after(hole.start, seps, + predicate=lambda m: m == ignored_match) + if starting: + should_keep = self.should_keep(ignored_match, to_keep, matches, filepart, hole, True) + if should_keep: + # pylint:disable=unpacking-non-sequence + try: + append, crop = should_keep + except TypeError: + append, crop = should_keep, should_keep + if append: + to_keep.append(ignored_match) + if crop: + hole.start = ignored_match.end + + for match in ignored_matches: + if self.should_remove(match, matches, filepart, hole, context): + to_remove.append(match) + for keep_match in to_keep: + if keep_match in to_remove: + to_remove.remove(keep_match) + + if hole and hole.value: + hole.name = self.match_name + hole.tags = self.match_tags + if self.alternative_match_name: + # Split and keep values that can be a title + titles = hole.split(title_seps, lambda m: m.value) + for title_match in list(titles[1:]): + previous_title = titles[titles.index(title_match) - 1] + separator = matches.input_string[previous_title.end:title_match.start] + if len(separator) == 1 and separator == '-' \ + and previous_title.raw[-1] not in seps \ + and title_match.raw[0] not in seps: + titles[titles.index(title_match) - 1].end = title_match.end + titles.remove(title_match) + else: + title_match.name = self.alternative_match_name + + else: + titles = [hole] + return titles, to_remove + + def when(self, matches, context): + ret = [] + to_remove = [] + + if matches.named(self.match_name, lambda match: 'expected' in match.tags): + return ret, to_remove + + fileparts = [filepart for filepart in list(marker_sorted(matches.markers.named('path'), matches)) + if not self.filepart_filter or self.filepart_filter(filepart, matches)] + + # Priorize fileparts containing the year + years_fileparts = [] + for filepart in fileparts: + year_match = matches.range(filepart.start, filepart.end, lambda match: match.name == 'year', 0) + if year_match: + years_fileparts.append(filepart) + + for filepart in fileparts: + try: + years_fileparts.remove(filepart) + except ValueError: + pass + titles = self.check_titles_in_filepart(filepart, matches, context) + if titles: + titles, to_remove_c = titles + ret.extend(titles) + to_remove.extend(to_remove_c) + break + + # Add title match in all fileparts containing the year. + for filepart in years_fileparts: + titles = self.check_titles_in_filepart(filepart, matches, context) + if titles: + # pylint:disable=unbalanced-tuple-unpacking + titles, to_remove_c = titles + ret.extend(titles) + to_remove.extend(to_remove_c) + + return ret, to_remove + + +class TitleFromPosition(TitleBaseRule): + """ + Add title match in existing matches + """ + dependency = [FilmTitleRule, SubtitlePrefixLanguageRule, SubtitleSuffixLanguageRule, SubtitleExtensionRule] + + properties = {'title': [None], 'alternative_title': [None]} + + def __init__(self): + super(TitleFromPosition, self).__init__('title', ['title'], 'alternative_title') + + def enabled(self, context): + return not is_disabled(context, 'alternative_title') + + +class PreferTitleWithYear(Rule): + """ + Prefer title where filepart contains year. + """ + dependency = TitleFromPosition + consequence = [RemoveMatch, AppendTags(['equivalent-ignore'])] + + properties = {'title': [None]} + + def when(self, matches, context): + with_year_in_group = [] + with_year = [] + titles = matches.named('title') + + for title_match in titles: + filepart = matches.markers.at_match(title_match, lambda marker: marker.name == 'path', 0) + if filepart: + year_match = matches.range(filepart.start, filepart.end, lambda match: match.name == 'year', 0) + if year_match: + group = matches.markers.at_match(year_match, lambda m: m.name == 'group') + if group: + with_year_in_group.append(title_match) + else: + with_year.append(title_match) + + to_tag = [] + if with_year_in_group: + title_values = set([title_match.value for title_match in with_year_in_group]) + to_tag.extend(with_year_in_group) + elif with_year: + title_values = set([title_match.value for title_match in with_year]) + to_tag.extend(with_year) + else: + title_values = set([title_match.value for title_match in titles]) + + to_remove = [] + for title_match in titles: + if title_match.value not in title_values: + to_remove.append(title_match) + return to_remove, to_tag diff --git a/libs/guessit/rules/properties/type.py b/libs/guessit/rules/properties/type.py new file mode 100644 index 000000000..6a2877ef9 --- /dev/null +++ b/libs/guessit/rules/properties/type.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +type property +""" +from rebulk import CustomRule, Rebulk, POST_PROCESS +from rebulk.match import Match + +from ..common.pattern import is_disabled +from ...rules.processors import Processors + + +def _type(matches, value): + """ + Define type match with given value. + :param matches: + :param value: + :return: + """ + matches.append(Match(len(matches.input_string), len(matches.input_string), name='type', value=value)) + + +def type_(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'type')) + rebulk = rebulk.rules(TypeProcessor) + + return rebulk + + +class TypeProcessor(CustomRule): + """ + Post processor to find file type based on all others found matches. + """ + priority = POST_PROCESS + + dependency = Processors + + properties = {'type': ['episode', 'movie']} + + def when(self, matches, context): # pylint:disable=too-many-return-statements + option_type = context.get('type', None) + if option_type: + return option_type + + episode = matches.named('episode') + season = matches.named('season') + absolute_episode = matches.named('absolute_episode') + episode_details = matches.named('episode_details') + + if episode or season or episode_details or absolute_episode: + return 'episode' + + film = matches.named('film') + if film: + return 'movie' + + year = matches.named('year') + date = matches.named('date') + + if date and not year: + return 'episode' + + bonus = matches.named('bonus') + if bonus and not year: + return 'episode' + + crc32 = matches.named('crc32') + anime_release_group = matches.named('release_group', lambda match: 'anime' in match.tags) + if crc32 and anime_release_group: + return 'episode' + + return 'movie' + + def then(self, matches, when_response, context): + _type(matches, when_response) diff --git a/libs/guessit/rules/properties/video_codec.py b/libs/guessit/rules/properties/video_codec.py new file mode 100644 index 000000000..1f8f75d37 --- /dev/null +++ b/libs/guessit/rules/properties/video_codec.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +video_codec and video_profile property +""" +from rebulk.remodule import re + +from rebulk import Rebulk, Rule, RemoveMatch + +from ..common import dash +from ..common.pattern import is_disabled +from ..common.validators import seps_after, seps_before, seps_surround + + +def video_codec(config): # pylint:disable=unused-argument + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk() + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]).string_defaults(ignore_case=True) + rebulk.defaults(name="video_codec", + tags=['source-suffix', 'streaming_service.suffix'], + disabled=lambda context: is_disabled(context, 'video_codec')) + + rebulk.regex(r'Rv\d{2}', value='RealVideo') + rebulk.regex('Mpe?g-?2', '[hx]-?262', value='MPEG-2') + rebulk.string("DVDivX", "DivX", value="DivX") + rebulk.string('XviD', value='Xvid') + rebulk.regex('VC-?1', value='VC-1') + rebulk.string('VP7', value='VP7') + rebulk.string('VP8', 'VP80', value='VP8') + rebulk.string('VP9', value='VP9') + rebulk.regex('[hx]-?263', value='H.263') + rebulk.regex('[hx]-?264(?:-?AVC(?:HD)?)?(?:-?SC)?', 'MPEG-?4(?:-?AVC(?:HD)?)', 'AVC(?:HD)?(?:-?SC)?', value='H.264') + rebulk.regex('[hx]-?265(?:-?HEVC)?', 'HEVC', value='H.265') + rebulk.regex('(?P<video_codec>hevc)(?P<color_depth>10)', value={'video_codec': 'H.265', 'color_depth': '10-bit'}, + tags=['video-codec-suffix'], children=True) + + # http://blog.mediacoderhq.com/h264-profiles-and-levels/ + # http://fr.wikipedia.org/wiki/H.264 + rebulk.defaults(name="video_profile", + validator=seps_surround, + disabled=lambda context: is_disabled(context, 'video_profile')) + + rebulk.string('BP', value='Baseline', tags='video_profile.rule') + rebulk.string('XP', 'EP', value='Extended', tags='video_profile.rule') + rebulk.string('MP', value='Main', tags='video_profile.rule') + rebulk.string('HP', 'HiP', value='High', tags='video_profile.rule') + rebulk.regex('Hi422P', value='High 4:2:2') + rebulk.regex('Hi444PP', value='High 4:4:4 Predictive') + rebulk.regex('Hi10P?', value='High 10') # no profile validation is required + + rebulk.string('DXVA', value='DXVA', name='video_api', + disabled=lambda context: is_disabled(context, 'video_api')) + + rebulk.defaults(name='color_depth', + validator=seps_surround, + disabled=lambda context: is_disabled(context, 'color_depth')) + rebulk.regex('12.?bits?', value='12-bit') + rebulk.regex('10.?bits?', 'YUV420P10', 'Hi10P?', value='10-bit') + rebulk.regex('8.?bits?', value='8-bit') + + rebulk.rules(ValidateVideoCodec, VideoProfileRule) + + return rebulk + + +class ValidateVideoCodec(Rule): + """ + Validate video_codec with source property or separated + """ + priority = 64 + consequence = RemoveMatch + + def enabled(self, context): + return not is_disabled(context, 'video_codec') + + def when(self, matches, context): + ret = [] + for codec in matches.named('video_codec'): + if not seps_before(codec) and \ + not matches.at_index(codec.start - 1, lambda match: 'video-codec-prefix' in match.tags): + ret.append(codec) + continue + if not seps_after(codec) and \ + not matches.at_index(codec.end + 1, lambda match: 'video-codec-suffix' in match.tags): + ret.append(codec) + continue + return ret + + +class VideoProfileRule(Rule): + """ + Rule to validate video_profile + """ + consequence = RemoveMatch + + def enabled(self, context): + return not is_disabled(context, 'video_profile') + + def when(self, matches, context): + profile_list = matches.named('video_profile', lambda match: 'video_profile.rule' in match.tags) + ret = [] + for profile in profile_list: + codec = matches.previous(profile, lambda match: match.name == 'video_codec') + if not codec: + codec = matches.next(profile, lambda match: match.name == 'video_codec') + if not codec: + ret.append(profile) + return ret diff --git a/libs/guessit/rules/properties/website.py b/libs/guessit/rules/properties/website.py new file mode 100644 index 000000000..6df16be65 --- /dev/null +++ b/libs/guessit/rules/properties/website.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Website property. +""" +from pkg_resources import resource_stream # @UnresolvedImport +from rebulk.remodule import re + +from rebulk import Rebulk, Rule, RemoveMatch +from ..common import seps +from ..common.formatters import cleanup +from ..common.pattern import is_disabled +from ..common.validators import seps_surround +from ...reutils import build_or_pattern + + +def website(config): + """ + Builder for rebulk object. + + :param config: rule configuration + :type config: dict + :return: Created Rebulk object + :rtype: Rebulk + """ + rebulk = Rebulk(disabled=lambda context: is_disabled(context, 'website')) + rebulk = rebulk.regex_defaults(flags=re.IGNORECASE).string_defaults(ignore_case=True) + rebulk.defaults(name="website") + + tlds = [l.strip().decode('utf-8') + for l in resource_stream('guessit', 'tlds-alpha-by-domain.txt').readlines() + if b'--' not in l][1:] # All registered domain extension + + safe_tlds = config['safe_tlds'] # For sure a website extension + safe_subdomains = config['safe_subdomains'] # For sure a website subdomain + safe_prefix = config['safe_prefixes'] # Those words before a tlds are sure + website_prefixes = config['prefixes'] + + rebulk.regex(r'(?:[^a-z0-9]|^)((?:'+build_or_pattern(safe_subdomains) + + r'\.)+(?:[a-z-]+\.)+(?:'+build_or_pattern(tlds) + + r'))(?:[^a-z0-9]|$)', + children=True) + rebulk.regex(r'(?:[^a-z0-9]|^)((?:'+build_or_pattern(safe_subdomains) + + r'\.)*[a-z-]+\.(?:'+build_or_pattern(safe_tlds) + + r'))(?:[^a-z0-9]|$)', + safe_subdomains=safe_subdomains, safe_tlds=safe_tlds, children=True) + rebulk.regex(r'(?:[^a-z0-9]|^)((?:'+build_or_pattern(safe_subdomains) + + r'\.)*[a-z-]+\.(?:'+build_or_pattern(safe_prefix) + + r'\.)+(?:'+build_or_pattern(tlds) + + r'))(?:[^a-z0-9]|$)', + safe_subdomains=safe_subdomains, safe_prefix=safe_prefix, tlds=tlds, children=True) + + rebulk.string(*website_prefixes, + validator=seps_surround, private=True, tags=['website.prefix']) + + class PreferTitleOverWebsite(Rule): + """ + If found match is more likely a title, remove website. + """ + consequence = RemoveMatch + + @staticmethod + def valid_followers(match): + """ + Validator for next website matches + """ + return any(name in ['season', 'episode', 'year'] for name in match.names) + + def when(self, matches, context): + to_remove = [] + for website_match in matches.named('website'): + safe = False + for safe_start in safe_subdomains + safe_prefix: + if website_match.value.lower().startswith(safe_start): + safe = True + break + if not safe: + suffix = matches.next(website_match, PreferTitleOverWebsite.valid_followers, 0) + if suffix: + to_remove.append(website_match) + return to_remove + + rebulk.rules(PreferTitleOverWebsite, ValidateWebsitePrefix) + + return rebulk + + +class ValidateWebsitePrefix(Rule): + """ + Validate website prefixes + """ + priority = 64 + consequence = RemoveMatch + + def when(self, matches, context): + to_remove = [] + for prefix in matches.tagged('website.prefix'): + website_match = matches.next(prefix, predicate=lambda match: match.name == 'website', index=0) + if (not website_match or + matches.holes(prefix.end, website_match.start, + formatter=cleanup, seps=seps, predicate=lambda match: match.value)): + to_remove.append(prefix) + return to_remove diff --git a/libs/guessit/test/__init__.py b/libs/guessit/test/__init__.py new file mode 100644 index 000000000..e5be370e4 --- /dev/null +++ b/libs/guessit/test/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name diff --git a/libs/guessit/test/config/dummy.txt b/libs/guessit/test/config/dummy.txt new file mode 100644 index 000000000..7d6ca31ba --- /dev/null +++ b/libs/guessit/test/config/dummy.txt @@ -0,0 +1 @@ +Not a configuration file
\ No newline at end of file diff --git a/libs/guessit/test/config/test.json b/libs/guessit/test/config/test.json new file mode 100644 index 000000000..22f45d2a6 --- /dev/null +++ b/libs/guessit/test/config/test.json @@ -0,0 +1,4 @@ +{ + "expected_title": ["The 100", "OSS 117"], + "yaml": false +} diff --git a/libs/guessit/test/config/test.yaml b/libs/guessit/test/config/test.yaml new file mode 100644 index 000000000..6a4dfe15b --- /dev/null +++ b/libs/guessit/test/config/test.yaml @@ -0,0 +1,4 @@ +expected_title: + - The 100 + - OSS 117 +yaml: True diff --git a/libs/guessit/test/config/test.yml b/libs/guessit/test/config/test.yml new file mode 100644 index 000000000..6a4dfe15b --- /dev/null +++ b/libs/guessit/test/config/test.yml @@ -0,0 +1,4 @@ +expected_title: + - The 100 + - OSS 117 +yaml: True diff --git a/libs/guessit/test/enable_disable_properties.yml b/libs/guessit/test/enable_disable_properties.yml new file mode 100644 index 000000000..e330e37db --- /dev/null +++ b/libs/guessit/test/enable_disable_properties.yml @@ -0,0 +1,335 @@ +? vorbis +: options: --exclude audio_codec + -audio_codec: Vorbis + +? DTS-ES +: options: --exclude audio_profile + audio_codec: DTS + -audio_profile: Extended Surround + +? DTS.ES +: options: --include audio_codec + audio_codec: DTS + -audio_profile: Extended Surround + +? 5.1 +? 5ch +? 6ch +: options: --exclude audio_channels + -audio_channels: '5.1' + +? Movie Title-x01-Other Title.mkv +? Movie Title-x01-Other Title +? directory/Movie Title-x01-Other Title/file.mkv +: options: --exclude bonus + -bonus: 1 + -bonus_title: Other Title + +? Title-x02-Bonus Title.mkv +: options: --include bonus + bonus: 2 + -bonus_title: Other Title + +? cd 1of3 +: options: --exclude cd + -cd: 1 + -cd_count: 3 + +? This.Is.Us +: options: --exclude country + title: This Is Us + -country: US + +? 2015.01.31 +: options: --exclude date + year: 2015 + -date: 2015-01-31 + +? Something 2 mar 2013) +: options: --exclude date + -date: 2013-03-02 + +? 2012 2009 S01E02 2015 # If no year is marked, the second one is guessed. +: options: --exclude year + -year: 2009 + +? Director's cut +: options: --exclude edition + -edition: Director's Cut + +? 2x5 +? 2X5 +? 02x05 +? 2X05 +? 02x5 +? S02E05 +? s02e05 +? s02e5 +? s2e05 +? s02ep05 +? s2EP5 +: options: --exclude season + -season: 2 + -episode: 5 + +? 2x6 +? 2X6 +? 02x06 +? 2X06 +? 02x6 +? S02E06 +? s02e06 +? s02e6 +? s2e06 +? s02ep06 +? s2EP6 +: options: --exclude episode + -season: 2 + -episode: 6 + +? serie Season 2 other +: options: --exclude season + -season: 2 + +? Some Dummy Directory/S02 Some Series/E01-Episode title.mkv +: options: --exclude episode_title + -episode_title: Episode title + season: 2 + episode: 1 + +? Another Dummy Directory/S02 Some Series/E01-Episode title.mkv +: options: --include season --include episode + -episode_title: Episode title + season: 2 + episode: 1 + +# pattern contains season and episode: it wont work enabling only one +? Some Series S03E01E02 +: options: --include episode + -season: 3 + -episode: [1, 2] + +# pattern contains season and episode: it wont work enabling only one +? Another Series S04E01E02 +: options: --include season + -season: 4 + -episode: [1, 2] + +? Show.Name.Season.4.Episode.1 +: options: --include episode + -season: 4 + episode: 1 + +? Another.Show.Name.Season.4.Episode.1 +: options: --include season + season: 4 + -episode: 1 + +? Some Series S01 02 03 +: options: --exclude season + -season: [1, 2, 3] + +? Some Series E01 02 04 +: options: --exclude episode + -episode: [1, 2, 4] + +? A very special episode s06 special +: options: -t episode --exclude episode_details + season: 6 + -episode_details: Special + +? S01D02.3-5-GROUP +: options: --exclude disc + -season: 1 + -disc: [2, 3, 4, 5] + -episode: [2, 3, 4, 5] + +? S01D02&4-6&8 +: options: --exclude season + -season: 1 + -disc: [2, 4, 5, 6, 8] + -episode: [2, 4, 5, 6, 8] + +? Film Title-f01-Series Title.mkv +: options: --exclude film + -film: 1 + -film_title: Film Title + +? Another Film Title-f01-Series Title.mkv +: options: --exclude film_title + film: 1 + -film_title: Film Title + +? English +? .ENG. +: options: --exclude language + -language: English + +? SubFrench +? SubFr +? STFr +: options: --exclude subtitle_language + -language: French + -subtitle_language: French + +? ST.FR +: options: --exclude subtitle_language + language: French + -subtitle_language: French + +? ENG.-.sub.FR +? ENG.-.FR Sub +: options: --include language + language: [English, French] + -subtitle_language: French + +? ENG.-.SubFR +: options: --include language + language: English + -subtitle_language: French + +? ENG.-.FRSUB +? ENG.-.FRSUBS +? ENG.-.FR-SUBS +: options: --include subtitle_language + -language: English + subtitle_language: French + +? DVD.Real.XViD +? DVD.fix.XViD +: options: --exclude other + -other: Proper + -proper_count: 1 + +? Part 3 +? Part III +? Part Three +? Part Trois +? Part3 +: options: --exclude part + -part: 3 + +? Some.Title.XViD-by.Artik[SEDG].avi +: options: --exclude release_group + -release_group: Artik[SEDG] + +? "[ABC] Some.Title.avi" +? some/folder/[ABC]Some.Title.avi +: options: --exclude release_group + -release_group: ABC + +? 360p +? 360px +? "360" +? +500x360 +: options: --exclude screen_size + -screen_size: 360p + +? 640x360 +: options: --exclude aspect_ratio + screen_size: 360p + -aspect_ratio: 1.778 + +? 8196x4320 +: options: --exclude screen_size + -screen_size: 4320p + -aspect_ratio: 1.897 + +? 4.3gb +: options: --exclude size + -size: 4.3GB + +? VhS_rip +? VHS.RIP +: options: --exclude source + -source: VHS + -other: Rip + +? DVD.RIP +: options: --include other + -source: DVD + -other: Rip + +? Title Only.avi +: options: --exclude title + -title: Title Only + +? h265 +? x265 +? h.265 +? x.265 +? hevc +: options: --exclude video_codec + -video_codec: H.265 + +? hevc10 +: options: --include color_depth + -video_codec: H.265 + -color_depth: 10-bit + +? HEVC-YUV420P10 +: options: --include color_depth + -video_codec: H.265 + color_depth: 10-bit + +? h265-HP +: options: --exclude video_profile + video_codec: H.265 + -video_profile: High + +? House.of.Cards.2013.S02E03.1080p.NF.WEBRip.DD5.1.x264-NTb.mkv +? House.of.Cards.2013.S02E03.1080p.Netflix.WEBRip.DD5.1.x264-NTb.mkv +: options: --exclude streaming_service + -streaming_service: Netflix + +? wawa.co.uk +: options: --exclude website + -website: wawa.co.uk + +? movie.mkv +: options: --exclude mimetype + -mimetype: video/x-matroska + +? another movie.mkv +: options: --exclude container + -container: mkv + +? series s02e01 +: options: --exclude type + -type: episode + +? series s02e01 +: options: --exclude type + -type: episode + +? Hotel.Hell.S01E01.720p.DD5.1.448kbps-ALANiS +: options: --exclude audio_bit_rate + -audio_bit_rate: 448Kbps + +? Katy Perry - Pepsi & Billboard Summer Beats Concert Series 2012 1080i HDTV 20 Mbps DD2.0 MPEG2-TrollHD.ts +: options: --exclude video_bit_rate + -video_bit_rate: 20Mbps + +? "[Figmentos] Monster 34 - At the End of Darkness [781219F1].mkv" +: options: --exclude crc32 + -crc32: 781219F1 + +? 1080p25 +: options: --exclude frame_rate + screen_size: 1080p + -frame_rate: 25fps + +? 1080p25 +: options: --exclude screen_size + -screen_size: 1080p + -frame_rate: 25fps + +? 1080p25 +: options: --include frame_rate + -screen_size: 1080p + -frame_rate: 25fps + +? 1080p 30fps +: options: --exclude screen_size + -screen_size: 1080p + frame_rate: 30fps diff --git a/libs/guessit/test/episodes.yml b/libs/guessit/test/episodes.yml new file mode 100644 index 000000000..97320c809 --- /dev/null +++ b/libs/guessit/test/episodes.yml @@ -0,0 +1,4496 @@ +? __default__ +: type: episode + +? Series/Californication/Season 2/Californication.2x05.Vaginatown.HDTV.XviD-0TV.avi +: title: Californication + season: 2 + episode: 5 + episode_title: Vaginatown + source: HDTV + video_codec: Xvid + release_group: 0TV + container: avi + +? Series/dexter/Dexter.5x02.Hello,.Bandit.ENG.-.sub.FR.HDTV.XviD-AlFleNi-TeaM.[tvu.org.ru].avi +: title: Dexter + season: 5 + episode: 2 + episode_title: Hello, Bandit + language: English + subtitle_language: French + source: HDTV + video_codec: Xvid + release_group: AlFleNi-TeaM + website: tvu.org.ru + container: avi + +? Series/Treme/Treme.1x03.Right.Place,.Wrong.Time.HDTV.XviD-NoTV.avi +: title: Treme + season: 1 + episode: 3 + episode_title: Right Place, Wrong Time + source: HDTV + video_codec: Xvid + release_group: NoTV + +? Series/Duckman/Duckman - S1E13 Joking The Chicken (unedited).avi +: title: Duckman + season: 1 + episode: 13 + episode_title: Joking The Chicken + +? Series/Simpsons/Saison 12 Français/Simpsons,.The.12x08.A.Bas.Le.Sergent.Skinner.FR.avi +: title: The Simpsons + season: 12 + episode: 8 + episode_title: A Bas Le Sergent Skinner + language: French + +? Series/Duckman/Duckman - 101 (01) - 20021107 - I, Duckman.avi +: title: Duckman + season: 1 + episode: 1 + episode_title: I, Duckman + date: 2002-11-07 + +? Series/Simpsons/Saison 12 Français/Simpsons,.The.12x08.A.Bas.Le.Sergent.Skinner.FR.avi +: title: The Simpsons + season: 12 + episode: 8 + episode_title: A Bas Le Sergent Skinner + language: French + +? Series/Futurama/Season 3 (mkv)/[™] Futurama - S03E22 - Le chef de fer à 30% ( 30 Percent Iron Chef ).mkv +: title: Futurama + season: 3 + episode: 22 + episode_title: Le chef de fer à 30% + +? Series/The Office/Season 6/The Office - S06xE01.avi +: title: The Office + season: 6 + episode: 1 + +? series/The Office/Season 4/The Office [401] Fun Run.avi +: title: The Office + season: 4 + episode: 1 + episode_title: Fun Run + +? Series/Mad Men Season 1 Complete/Mad.Men.S01E01.avi +: title: Mad Men + season: 1 + episode: 1 + other: Complete + +? series/Psych/Psych S02 Season 2 Complete English DVD/Psych.S02E02.65.Million.Years.Off.avi +: title: Psych + season: 2 + episode: 2 + episode_title: 65 Million Years Off + language: english + source: DVD + other: Complete + +? series/Psych/Psych S02 Season 2 Complete English DVD/Psych.S02E03.Psy.Vs.Psy.Français.srt +: title: Psych + season: 2 + episode: 3 + episode_title: Psy Vs Psy + source: DVD + language: English + subtitle_language: French + other: Complete + +? Series/Pure Laine/Pure.Laine.1x01.Toutes.Couleurs.Unies.FR.(Québec).DVB-Kceb.[tvu.org.ru].avi +: title: Pure Laine + season: 1 + episode: 1 + episode_title: Toutes Couleurs Unies + source: Digital TV + release_group: Kceb + language: french + website: tvu.org.ru + +? Series/Pure Laine/2x05 - Pure Laine - Je Me Souviens.avi +: title: Pure Laine + season: 2 + episode: 5 + episode_title: Je Me Souviens + +? Series/Tout sur moi/Tout sur moi - S02E02 - Ménage à trois (14-01-2008) [Rip by Ampli].avi +: title: Tout sur moi + season: 2 + episode: 2 + episode_title: Ménage à trois + date: 2008-01-14 + +? The.Mentalist.2x21.18-5-4.ENG.-.sub.FR.HDTV.XviD-AlFleNi-TeaM.[tvu.org.ru].avi +: title: The Mentalist + season: 2 + episode: 21 + episode_title: 18-5-4 + language: english + subtitle_language: french + source: HDTV + video_codec: Xvid + release_group: AlFleNi-TeaM + website: tvu.org.ru + +? series/__ Incomplete __/Dr Slump (Catalan)/Dr._Slump_-_003_DVB-Rip_Catalan_by_kelf.avi +: title: Dr Slump + episode: 3 + source: Digital TV + other: Rip + language: catalan + +# Disabling this test because it just doesn't looks like a serie ... +#? series/Ren and Stimpy - Black_hole_[DivX].avi +#: title: Ren and Stimpy +# episode_title: Black hole +# video_codec: DivX + +# Disabling this test because it just doesn't looks like a serie ... +# ? Series/Walt Disney/Donald.Duck.-.Good.Scouts.[www.bigernie.jump.to].avi +#: title: Donald Duck +# episode_title: Good Scouts +# website: www.bigernie.jump.to + +? Series/Neverwhere/Neverwhere.05.Down.Street.[tvu.org.ru].avi +: title: Neverwhere + episode: 5 + episode_title: Down Street + website: tvu.org.ru + +? Series/South Park/Season 4/South.Park.4x07.Cherokee.Hair.Tampons.DVDRip.[tvu.org.ru].avi +: title: South Park + season: 4 + episode: 7 + episode_title: Cherokee Hair Tampons + source: DVD + other: Rip + website: tvu.org.ru + +? Series/Kaamelott/Kaamelott - Livre V - Ep 23 - Le Forfait.avi +: title: Kaamelott + alternative_title: Livre V + episode: 23 + episode_title: Le Forfait + +? Series/Duckman/Duckman - 110 (10) - 20021218 - Cellar Beware.avi +: title: Duckman + season: 1 + episode: 10 + date: 2002-12-18 + episode_title: Cellar Beware + +# Removing this test because it doesn't look like a series +# ? Series/Ren & Stimpy/Ren And Stimpy - Onward & Upward-Adult Party Cartoon.avi +# : title: Ren And Stimpy +# episode_title: Onward & Upward-Adult Party Cartoon + +? Series/Breaking Bad/Minisodes/Breaking.Bad.(Minisodes).01.Good.Cop.Bad.Cop.WEBRip.XviD.avi +: title: Breaking Bad + episode_format: Minisode + episode: 1 + episode_title: Good Cop Bad Cop + source: Web + other: Rip + video_codec: Xvid + +? Series/My Name Is Earl/My.Name.Is.Earl.S01Extras.-.Bad.Karma.DVDRip.XviD.avi +: title: My Name Is Earl + season: 1 + episode_title: Extras - Bad Karma + source: DVD + other: Rip + episode_details: Extras + video_codec: Xvid + +? series/Freaks And Geeks/Season 1/Episode 4 - Kim Kelly Is My Friend-eng(1).srt +: title: Freaks And Geeks + season: 1 + episode: 4 + episode_title: Kim Kelly Is My Friend + subtitle_language: English # This is really a subtitle_language, despite guessit 1.x assert for language. + +? /mnt/series/The Big Bang Theory/S01/The.Big.Bang.Theory.S01E01.mkv +: title: The Big Bang Theory + season: 1 + episode: 1 + +? /media/Parks_and_Recreation-s03-e01.mkv +: title: Parks and Recreation + season: 3 + episode: 1 + +? /media/Parks_and_Recreation-s03-e02-Flu_Season.mkv +: title: Parks and Recreation + season: 3 + episode_title: Flu Season + episode: 2 + +? /media/Parks_and_Recreation-s03-x01.mkv +: title: Parks and Recreation + season: 3 + episode: 1 + +? /media/Parks_and_Recreation-s03-x02-Gag_Reel.mkv +: title: Parks and Recreation + season: 3 + episode: 2 + episode_title: Gag Reel + +? /media/Band_of_Brothers-e01-Currahee.mkv +: title: Band of Brothers + episode: 1 + episode_title: Currahee + +? /media/Band_of_Brothers-x02-We_Stand_Alone_Together.mkv +: title: Band of Brothers + bonus: 2 + bonus_title: We Stand Alone Together + +? /TV Shows/Mad.M-5x9.mkv +: title: Mad M + season: 5 + episode: 9 + +? /TV Shows/new.girl.117.hdtv-lol.mp4 +: title: new girl + season: 1 + episode: 17 + source: HDTV + release_group: lol + +? Kaamelott - 5x44x45x46x47x48x49x50.avi +: title: Kaamelott + season: 5 + episode: [44, 45, 46, 47, 48, 49, 50] + +? Example S01E01-02.avi +? Example S01E01E02.avi +: title: Example + season: 1 + episode: [1, 2] + +? Series/Baccano!/Baccano!_-_T1_-_Trailer_-_[Ayu](dae8173e).mkv +: title: Baccano! + other: Trailer + release_group: Ayu + episode_title: T1 + crc32: dae8173e + +? Series/Doctor Who (2005)/Season 06/Doctor Who (2005) - S06E01 - The Impossible Astronaut (1).avi +: title: Doctor Who + year: 2005 + season: 6 + episode: 1 + episode_title: The Impossible Astronaut + +? The Sopranos - [05x07] - In Camelot.mp4 +: title: The Sopranos + season: 5 + episode: 7 + episode_title: In Camelot + +? The.Office.(US).1x03.Health.Care.HDTV.XviD-LOL.avi +: title: The Office + country: US + season: 1 + episode: 3 + episode_title: Health Care + source: HDTV + video_codec: Xvid + release_group: LOL + +? /Volumes/data-1/Series/Futurama/Season 3/Futurama_-_S03_DVD_Bonus_-_Deleted_Scenes_Part_3.ogm +: title: Futurama + season: 3 + part: 3 + other: Bonus + episode_title: Deleted Scenes + source: DVD + +? Ben.and.Kate.S01E02.720p.HDTV.X264-DIMENSION.mkv +: title: Ben and Kate + season: 1 + episode: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: DIMENSION + +? /volume1/TV Series/Drawn Together/Season 1/Drawn Together 1x04 Requiem for a Reality Show.avi +: title: Drawn Together + season: 1 + episode: 4 + episode_title: Requiem for a Reality Show + +? Sons.of.Anarchy.S05E06.720p.WEB.DL.DD5.1.H.264-CtrlHD.mkv +: title: Sons of Anarchy + season: 5 + episode: 6 + screen_size: 720p + source: Web + audio_channels: "5.1" + audio_codec: Dolby Digital + video_codec: H.264 + release_group: CtrlHD + +? /media/bdc64bfe-e36f-4af8-b550-e6fd2dfaa507/TV_Shows/Doctor Who (2005)/Saison 6/Doctor Who (2005) - S06E13 - The Wedding of River Song.mkv +: title: Doctor Who + season: 6 + episode: 13 + year: 2005 + episode_title: The Wedding of River Song + uuid: bdc64bfe-e36f-4af8-b550-e6fd2dfaa507 + +? /mnt/videos/tvshows/Doctor Who/Season 06/E13 - The Wedding of River Song.mkv +: title: Doctor Who + season: 6 + episode: 13 + episode_title: The Wedding of River Song + +? The.Simpsons.S24E03.Adventures.in.Baby-Getting.720p.WEB-DL.DD5.1.H.264-CtrlHD.mkv +: title: The Simpsons + season: 24 + episode: 3 + episode_title: Adventures in Baby-Getting + screen_size: 720p + source: Web + audio_channels: "5.1" + audio_codec: Dolby Digital + video_codec: H.264 + release_group: CtrlHD + +? /home/disaster/Videos/TV/Merlin/merlin_2008.5x02.arthurs_bane_part_two.repack.720p_hdtv_x264-fov.mkv +: title: merlin + season: 5 + episode: 2 + part: 2 + episode_title: arthurs bane + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: fov + year: 2008 + other: Proper + proper_count: 1 + +? "Da Vinci's Demons - 1x04 - The Magician.mkv" +: title: "Da Vinci's Demons" + season: 1 + episode: 4 + episode_title: The Magician + +? CSI.S013E18.Sheltered.720p.WEB-DL.DD5.1.H.264.mkv +: title: CSI + season: 13 + episode: 18 + episode_title: Sheltered + screen_size: 720p + source: Web + audio_channels: "5.1" + audio_codec: Dolby Digital + video_codec: H.264 + +? Game of Thrones S03E06 1080i HDTV DD5.1 MPEG2-TrollHD.ts +: title: Game of Thrones + season: 3 + episode: 6 + screen_size: 1080i + source: HDTV + audio_channels: "5.1" + audio_codec: Dolby Digital + video_codec: MPEG-2 + release_group: TrollHD + +? gossip.girl.s01e18.hdtv.xvid-2hd.eng.srt +: title: gossip girl + season: 1 + episode: 18 + source: HDTV + video_codec: Xvid + release_group: 2hd + subtitle_language: english + +? Wheels.S03E01E02.720p.HDTV.x264-IMMERSE.mkv +: title: Wheels + season: 3 + episode: [1, 2] + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: IMMERSE + +? Wheels.S03E01-02.720p.HDTV.x264-IMMERSE.mkv +: title: Wheels + season: 3 + episode: [1, 2] + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: IMMERSE + +? Wheels.S03E01-E02.720p.HDTV.x264-IMMERSE.mkv +: title: Wheels + season: 3 + episode: [1, 2] + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: IMMERSE + +? Wheels.S03E01-04.720p.HDTV.x264-IMMERSE.mkv +: title: Wheels + season: 3 + episode: [1, 2, 3, 4] + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: IMMERSE + +? Marvels.Agents.of.S.H.I.E.L.D-S01E06.720p.HDTV.X264-DIMENSION.mkv +: title: Marvels Agents of S.H.I.E.L.D + season: 1 + episode: 6 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: DIMENSION + +? Marvels.Agents.of.S.H.I.E.L.D.S01E06.720p.HDTV.X264-DIMENSION.mkv +: title: Marvels Agents of S.H.I.E.L.D. + season: 1 + episode: 6 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: DIMENSION + +? Marvels.Agents.of.S.H.I.E.L.D..S01E06.720p.HDTV.X264-DIMENSION.mkv +: title: Marvels Agents of S.H.I.E.L.D. + season: 1 + episode: 6 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: DIMENSION + +? Series/Friday Night Lights/Season 1/Friday Night Lights S01E19 - Ch-Ch-Ch-Ch-Changes.avi +: title: Friday Night Lights + season: 1 + episode: 19 + episode_title: Ch-Ch-Ch-Ch-Changes + +? Dexter Saison VII FRENCH.BDRip.XviD-MiND.nfo +: title: Dexter + season: 7 + video_codec: Xvid + language: French + source: Blu-ray + other: Rip + release_group: MiND + +? Dexter Saison sept FRENCH.BDRip.XviD-MiND.nfo +: title: Dexter + season: 7 + video_codec: Xvid + language: French + source: Blu-ray + other: Rip + release_group: MiND + +? "Pokémon S16 - E29 - 1280*720 HDTV VF.mkv" +: title: Pokémon + source: HDTV + language: French + season: 16 + episode: 29 + screen_size: 720p + +? One.Piece.E576.VOSTFR.720p.HDTV.x264-MARINE-FORD.mkv +: episode: 576 + video_codec: H.264 + source: HDTV + title: One Piece + release_group: MARINE-FORD + subtitle_language: French + screen_size: 720p + +? Dexter.S08E12.FINAL.MULTi.1080p.BluRay.x264-MiND.mkv +: video_codec: H.264 + episode: 12 + season: 8 + source: Blu-ray + title: Dexter + episode_details: Final + language: Multiple languages + release_group: MiND + screen_size: 1080p + +? One Piece - E623 VOSTFR HD [www.manga-ddl-free.com].mkv +: website: www.manga-ddl-free.com + episode: 623 + subtitle_language: French + title: One Piece + other: HD + +? Falling Skies Saison 1.HDLight.720p.x264.VFF.mkv +: language: French + screen_size: 720p + season: 1 + title: Falling Skies + video_codec: H.264 + other: Micro HD + +? Sleepy.Hollow.S01E09.720p.WEB-DL.DD5.1.H.264-BP.mkv +: episode: 9 + video_codec: H.264 + source: Web + title: Sleepy Hollow + audio_channels: "5.1" + screen_size: 720p + season: 1 +# video_profile: BP # TODO: related to https://github.com/guessit-io/guessit/issues/458#issuecomment-305719715 + audio_codec: Dolby Digital + +? Sleepy.Hollow.S01E09.720p.WEB-DL.DD5.1.H.264-BS.mkv +: episode: 9 + video_codec: H.264 + source: Web + title: Sleepy Hollow + audio_channels: "5.1" + screen_size: 720p + season: 1 + release_group: BS + audio_codec: Dolby Digital + +? Battlestar.Galactica.S00.Pilot.FRENCH.DVDRip.XviD-NOTAG.avi +: title: Battlestar Galactica + season: 0 + episode_details: Pilot + episode_title: Pilot + language: French + source: DVD + other: Rip + video_codec: Xvid + release_group: NOTAG + +? The Big Bang Theory S00E00 Unaired Pilot VOSTFR TVRip XviD-VioCs +: title: The Big Bang Theory + season: 0 + episode: 0 + subtitle_language: French + source: TV + other: Rip + video_codec: Xvid + release_group: VioCs + episode_details: [Unaired, Pilot] + +? The Big Bang Theory S01E00 PROPER Unaired Pilot TVRip XviD-GIGGITY +: title: The Big Bang Theory + season: 1 + episode: 0 + source: TV + video_codec: Xvid + release_group: GIGGITY + other: [Proper, Rip] + proper_count: 1 + episode_details: [Unaired, Pilot] + +? Pawn.Stars.S2014E18.720p.HDTV.x264-KILLERS +: title: Pawn Stars + season: 2014 + year: 2014 + episode: 18 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: KILLERS + +? 2.Broke.Girls.S03E10.480p.HDTV.x264-mSD.mkv +: title: 2 Broke Girls + season: 3 + episode: 10 + screen_size: 480p + source: HDTV + video_codec: H.264 + release_group: mSD + +? the.100.109.hdtv-lol.mp4 +: title: the 100 + season: 1 + episode: 9 + source: HDTV + release_group: lol + +? Criminal.Minds.5x03.Reckoner.ENG.-.sub.FR.HDTV.XviD-STi.[tvu.org.ru].avi +: title: Criminal Minds + language: English + subtitle_language: French + season: 5 + episode: 3 + video_codec: Xvid + source: HDTV + website: tvu.org.ru + release_group: STi + episode_title: Reckoner + +? 03-Criminal.Minds.avi +: title: Criminal Minds + episode: 3 + +? '[Evil-Saizen]_Laughing_Salesman_14_[DVD][1C98686A].mkv' +: crc32: 1C98686A + episode: 14 + source: DVD + release_group: Evil-Saizen + title: Laughing Salesman + +? '[Kaylith] Zankyou no Terror - 04 [480p][B4D4514E].mp4' +: crc32: B4D4514E + episode: 4 + release_group: Kaylith + screen_size: 480p + title: Zankyou no Terror + +? '[PuyaSubs!] Seirei Tsukai no Blade Dance - 05 [720p][32DD560E].mkv' +: crc32: 32DD560E + episode: 5 + release_group: PuyaSubs! + screen_size: 720p + title: Seirei Tsukai no Blade Dance + +? '[Doremi].Happiness.Charge.Precure.27.[1280x720].[DC91581A].mkv' +: crc32: DC91581A + episode: 27 + release_group: Doremi + screen_size: 720p + title: Happiness Charge Precure + +? "[Daisei] Free!:Iwatobi Swim Club - 01 ~ (BD 720p 10-bit AAC) [99E8E009].mkv" +: audio_codec: AAC + crc32: 99E8E009 + episode: 1 + source: Blu-ray + release_group: Daisei + screen_size: 720p + title: Free!:Iwatobi Swim Club + color_depth: 10-bit + +? '[Tsundere] Boku wa Tomodachi ga Sukunai - 03 [BDRip h264 1920x1080 10bit FLAC][AF0C22CC].mkv' +: audio_codec: FLAC + crc32: AF0C22CC + episode: 3 + source: Blu-ray + release_group: Tsundere + screen_size: 1080p + title: Boku wa Tomodachi ga Sukunai + video_codec: H.264 + color_depth: 10-bit + +? '[t.3.3.d]_Mikakunin_de_Shinkoukei_-_12_[720p][5DDC1352].mkv' +: crc32: 5DDC1352 + episode: 12 + screen_size: 720p + title: Mikakunin de Shinkoukei + release_group: t.3.3.d + +? '[Anime-Koi] Sabagebu! - 06 [h264-720p][ABB3728A].mkv' +: crc32: ABB3728A + episode: 6 + release_group: Anime-Koi + screen_size: 720p + title: Sabagebu! + video_codec: H.264 + +? '[aprm-Diogo4D] [BD][1080p] Nagi no Asukara 08 [4D102B7C].mkv' +: crc32: 4D102B7C + episode: 8 + source: Blu-ray + release_group: aprm-Diogo4D + screen_size: 1080p + title: Nagi no Asukara + +? '[Akindo-SSK] Zankyou no Terror - 05 [720P][Sub_ITA][F5CCE87C].mkv' +: crc32: F5CCE87C + episode: 5 + release_group: Akindo-SSK + screen_size: 720p + title: Zankyou no Terror + subtitle_language: it + +? Naruto Shippuden Episode 366 VOSTFR.avi +: episode: 366 + title: Naruto Shippuden + subtitle_language: fr + +? Naruto Shippuden Episode 366v2 VOSTFR.avi +: episode: 366 + version: 2 + title: Naruto Shippuden + subtitle_language: fr + +? '[HorribleSubs] Ao Haru Ride - 06 [480p].mkv' +: episode: 6 + release_group: HorribleSubs + screen_size: 480p + title: Ao Haru Ride + +? '[DeadFish] Tari Tari - 01 [BD][720p][AAC].mp4' +: audio_codec: AAC + episode: 1 + source: Blu-ray + release_group: DeadFish + screen_size: 720p + title: Tari Tari + +? '[NoobSubs] Sword Art Online II 06 (720p 8bit AAC).mp4' +: audio_codec: AAC + episode: 6 + release_group: NoobSubs + screen_size: 720p + title: Sword Art Online II + color_depth: 8-bit + +? '[DeadFish] 01 - Tari Tari [BD][720p][AAC].mp4' +: audio_codec: AAC + episode: 1 + source: Blu-ray + release_group: DeadFish + screen_size: 720p + title: Tari Tari + +? '[NoobSubs] 06 Sword Art Online II (720p 8bit AAC).mp4' +: audio_codec: AAC + episode: 6 + release_group: NoobSubs + screen_size: 720p + title: Sword Art Online II + color_depth: 8-bit + +? '[DeadFish] 12 - Tari Tari [BD][720p][AAC].mp4' +: audio_codec: AAC + episode: 12 + source: Blu-ray + release_group: DeadFish + screen_size: 720p + title: Tari Tari + +? Something.Season.2.1of4.Ep.Title.HDTV.torrent +: episode_count: 4 + episode: 1 + source: HDTV + season: 2 + title: Something + episode_title: Title + container: torrent + +? Something.Season.2of5.3of9.Ep.Title.HDTV.torrent +: episode_count: 9 + episode: 3 + source: HDTV + season: 2 + season_count: 5 + title: Something + episode_title: Title + container: torrent + +? Something.Other.Season.3of5.Complete.HDTV.torrent +: source: HDTV + other: Complete + season: 3 + season_count: 5 + title: Something Other + container: torrent + +? Something.Other.Season.1-3.avi +: season: [1, 2, 3] + title: Something Other + +? Something.Other.Season.1&3.avi +: season: [1, 3] + title: Something Other + +? Something.Other.Season.1&3-1to12ep.avi +: season: [1, 3] + title: Something Other + +? W2Test.123.HDTV.XViD-FlexGet +: episode: 23 + season: 1 + source: HDTV + release_group: FlexGet + title: W2Test + video_codec: Xvid + +? W2Test.123.HDTV.XViD-FlexGet +: options: --episode-prefer-number + episode: 123 + source: HDTV + release_group: FlexGet + title: W2Test + video_codec: Xvid + +? FooBar.0307.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + season: 3 + title: FooBar + +? FooBar.0307.PDTV-FlexGet +? FooBar.307.PDTV-FlexGet +: options: --episode-prefer-number + episode: 307 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.07.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.7.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.0307.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + season: 3 + title: FooBar + +? FooBar.307.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + season: 3 + title: FooBar + +? FooBar.07.PDTV-FlexGet +: episode: 7 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.07v4.PDTV-FlexGet +: episode: 7 + version: 4 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.7.PDTV-FlexGet +: source: Digital TV + release_group: FlexGet + title: FooBar 7 + type: movie + +? FooBar.7.PDTV-FlexGet +: options: -t episode + episode: 7 + source: Digital TV + release_group: FlexGet + title: FooBar + +? FooBar.7v3.PDTV-FlexGet +: options: -t episode + episode: 7 + version: 3 + source: Digital TV + release_group: FlexGet + title: FooBar + +? Test.S02E01.hdtv.real.proper +: episode: 1 + source: HDTV + other: Proper + proper_count: 2 + season: 2 + title: Test + +? Real.Test.S02E01.hdtv.proper +: episode: 1 + source: HDTV + other: Proper + proper_count: 1 + season: 2 + title: Real Test + +? Test.Real.S02E01.hdtv.proper +: episode: 1 + source: HDTV + other: Proper + proper_count: 1 + season: 2 + title: Test Real + +? Test.S02E01.hdtv.proper +: episode: 1 + source: HDTV + other: Proper + proper_count: 1 + season: 2 + title: Test + +? Test.S02E01.hdtv.real.repack.proper +: episode: 1 + source: HDTV + other: Proper + proper_count: 3 + season: 2 + title: Test + +? Date.Show.03-29-2012.HDTV.XViD-FlexGet +: date: 2012-03-29 + source: HDTV + release_group: FlexGet + title: Date Show + video_codec: Xvid + +? Something.1x5.Season.Complete-FlexGet +: episode: 5 + other: Complete + season: 1 + title: Something + release_group: FlexGet + +? Something Seasons 1 & 2 - Complete +: other: Complete + season: + - 1 + - 2 + title: Something + +? Something Seasons 4 Complete +: other: Complete + season: 4 + title: Something + +? Something.1xAll.Season.Complete-FlexGet +: other: Complete + season: 1 + title: Something + release_group: FlexGet + +? Something.1xAll-FlexGet +: other: Complete + season: 1 + title: Something + release_group: FlexGet + +? FlexGet.US.S2013E14.Title.Here.720p.HDTV.AAC5.1.x264-NOGRP +: audio_channels: '5.1' + audio_codec: AAC + country: US + episode: 14 + source: HDTV + release_group: NOGRP + screen_size: 720p + season: 2013 + title: FlexGet + episode_title: Title Here + video_codec: H.264 + year: 2013 + +? FlexGet.14.of.21.Title.Here.720p.HDTV.AAC5.1.x264-NOGRP +: audio_channels: '5.1' + audio_codec: AAC + episode_count: 21 + episode: 14 + source: HDTV + release_group: NOGRP + screen_size: 720p + title: FlexGet + episode_title: Title Here + video_codec: H.264 + +? FlexGet.Series.2013.14.of.21.Title.Here.720p.HDTV.AAC5.1.x264-NOGRP +: audio_channels: '5.1' + audio_codec: AAC + episode_count: 21 + episode: 14 + source: HDTV + release_group: NOGRP + screen_size: 720p + season: 2013 + title: FlexGet + episode_title: Title Here + video_codec: H.264 + year: 2013 + +? Something.S04E05E09 +: episode: # 1.x guessit this as a range from 5 to 9. But not sure if it should ... + - 5 + - 9 + season: 4 + title: Something + +? FooBar 360 1080i +: options: --episode-prefer-number + episode: 360 + screen_size: 1080i + title: FooBar + +? FooBar 360 1080i +: episode: 60 + season: 3 + screen_size: 1080i + title: FooBar + +? FooBar 360 +: season: 3 + episode: 60 + title: FooBar + -screen_size: 360p + +? BarFood christmas special HDTV +: options: --expected-title BarFood + source: HDTV + title: BarFood + episode_title: christmas special + episode_details: Special + +? Something.2008x12.13-FlexGet +: title: Something + date: 2008-12-13 + episode_title: FlexGet + +? '[Ignored] Test 12' +: episode: 12 + release_group: Ignored + title: Test + +? '[FlexGet] Test 12' +: episode: 12 + release_group: FlexGet + title: Test + +? Test.13.HDTV-Ignored +: episode: 13 + source: HDTV + release_group: Ignored + title: Test + +? Test.13.HDTV-Ignored +: options: --expected-series test + episode: 13 + source: HDTV + release_group: Ignored + title: Test + +? Test.13.HDTV-Ignored +: title: Test + episode: 13 + source: HDTV + release_group: Ignored + +? Test.13.HDTV-Ignored +: episode: 13 + source: HDTV + release_group: Ignored + title: Test + +? Test.13.HDTV-FlexGet +: episode: 13 + source: HDTV + release_group: FlexGet + title: Test + +? Test.14.HDTV-Name +: episode: 14 + source: HDTV + release_group: Name + title: Test + +? Real.Time.With.Bill.Maher.2014.10.31.HDTV.XviD-AFG.avi +: date: 2014-10-31 + source: HDTV + release_group: AFG + title: Real Time With Bill Maher + video_codec: Xvid + +? Arrow.S03E21.Al.Sah-Him.1080p.WEB-DL.DD5.1.H.264-BS.mkv +: title: Arrow + season: 3 + episode: 21 + episode_title: Al Sah-Him + screen_size: 1080p + audio_codec: Dolby Digital + audio_channels: "5.1" + video_codec: H.264 + release_group: BS + source: Web + +? How to Make It in America - S02E06 - I'm Sorry, Who's Yosi?.mkv +: title: How to Make It in America + season: 2 + episode: 6 + episode_title: I'm Sorry, Who's Yosi? + +? 24.S05E07.FRENCH.DVDRip.XviD-FiXi0N.avi +: episode: 7 + source: DVD + other: Rip + language: fr + season: 5 + title: '24' + video_codec: Xvid + release_group: FiXi0N + +? 12.Monkeys.S01E12.FRENCH.BDRip.x264-VENUE.mkv +: episode: 12 + source: Blu-ray + other: Rip + language: fr + release_group: VENUE + season: 1 + title: 12 Monkeys + video_codec: H.264 + +? 90.Day.Fiance.S02E07.I.Have.To.Tell.You.Something.720p.HDTV.x264-W4F +: episode: 7 + source: HDTV + screen_size: 720p + season: 2 + title: 90 Day Fiance + episode_title: I Have To Tell You Something + release_group: W4F + +? Doctor.Who.2005.S04E06.FRENCH.LD.DVDRip.XviD-TRACKS.avi +: episode: 6 + source: DVD + language: fr + release_group: TRACKS + season: 4 + title: Doctor Who + other: [Line Dubbed, Rip] + video_codec: Xvid + year: 2005 + +? Astro.Le.Petit.Robot.S01E01+02.FRENCH.DVDRiP.X264.INT-BOOLZ.mkv +: episode: [1, 2] + source: DVD + other: Rip + language: fr + release_group: INT-BOOLZ + season: 1 + title: Astro Le Petit Robot + video_codec: H.264 + +? Annika.Bengtzon.2012.E01.Le.Testament.De.Nobel.FRENCH.DVDRiP.XViD-STVFRV.avi +: episode: 1 + source: DVD + other: Rip + language: fr + release_group: STVFRV + title: Annika Bengtzon + episode_title: Le Testament De Nobel + video_codec: Xvid + year: 2012 + +? Dead.Set.02.FRENCH.LD.DVDRip.XviD-EPZ.avi +: episode: 2 + source: DVD + language: fr + other: [Line Dubbed, Rip] + release_group: EPZ + title: Dead Set + video_codec: Xvid + +? Phineas and Ferb S01E00 & S01E01 & S01E02 +: episode: [0, 1, 2] + season: 1 + title: Phineas and Ferb + +? Show.Name.S01E02.S01E03.HDTV.XViD.Etc-Group +: episode: [2, 3] + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - S01E02 - S01E03 - S01E04 - Ep Name +: episode: [2, 3, 4] + season: 1 + title: Show Name + episode_title: Ep Name + +? Show.Name.1x02.1x03.HDTV.XViD.Etc-Group +: episode: [2, 3] + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - 1x02 - 1x03 - 1x04 - Ep Name +: episode: [2, 3, 4] + season: 1 + title: Show Name + episode_title: Ep Name + +? Show.Name.S01E02.HDTV.XViD.Etc-Group +: episode: 2 + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - S01E02 - My Ep Name +: episode: 2 + season: 1 + title: Show Name + episode_title: My Ep Name + +? Show Name - S01.E03 - My Ep Name +: episode: 3 + season: 1 + title: Show Name + episode_title: My Ep Name + +? Show.Name.S01E02E03.HDTV.XViD.Etc-Group +: episode: [2, 3] + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - S01E02-03 - My Ep Name +: episode: [2, 3] + season: 1 + title: Show Name + episode_title: My Ep Name + +? Show.Name.S01.E02.E03 +: episode: [2, 3] + season: 1 + title: Show Name + +? Show_Name.1x02.HDTV_XViD_Etc-Group +: episode: 2 + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - 1x02 - My Ep Name +: episode: 2 + season: 1 + title: Show Name + episode_title: My Ep Name + +? Show_Name.1x02x03x04.HDTV_XViD_Etc-Group +: episode: [2, 3, 4] + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show Name - 1x02-03-04 - My Ep Name +: episode: [2, 3, 4] + season: 1 + title: Show Name + episode_title: My Ep Name + +# 1x guess this as episode 100 but 101 as episode 1 season 1. +? Show.Name.100.Event.2010.11.23.HDTV.XViD.Etc-Group +: date: 2010-11-23 + season: 1 + episode: 0 + source: HDTV + release_group: Etc-Group + title: Show Name + episode_title: Event + video_codec: Xvid + +? Show.Name.101.Event.2010.11.23.HDTV.XViD.Etc-Group +: date: 2010-11-23 + season: 1 + episode: 1 + source: HDTV + release_group: Etc-Group + title: Show Name + episode_title: Event + video_codec: Xvid + +? Show.Name.2010.11.23.HDTV.XViD.Etc-Group +: date: 2010-11-23 + source: HDTV + release_group: Etc-Group + title: Show Name + +? Show Name - 2010-11-23 - Ep Name +: date: 2010-11-23 + title: Show Name + episode_title: Ep Name + +? Show Name Season 1 Episode 2 Ep Name +: episode: 2 + season: 1 + title: Show Name + episode_title: Ep Name + +? Show.Name.S01.HDTV.XViD.Etc-Group +: source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? Show.Name.E02-03 +: episode: [2, 3] + title: Show Name + +? Show.Name.E02.2010 +: episode: 2 + year: 2010 + title: Show Name + +? Show.Name.E23.Test +: episode: 23 + title: Show Name + episode_title: Test + +? Show.Name.Part.3.HDTV.XViD.Etc-Group +: part: 3 + title: Show Name + source: HDTV + video_codec: Xvid + release_group: Etc-Group + type: movie + # Fallback to movie type because we can't tell it's a series ... + +? Show.Name.Part.1.and.Part.2.Blah-Group +: part: [1, 2] + title: Show Name + type: movie + # Fallback to movie type because we can't tell it's a series ... + +? Show Name - 01 - Ep Name +: episode: 1 + title: Show Name + episode_title: Ep Name + +? 01 - Ep Name +: episode: 1 + title: Ep Name + +? Show.Name.102.HDTV.XViD.Etc-Group +: episode: 2 + source: HDTV + release_group: Etc-Group + season: 1 + title: Show Name + video_codec: Xvid + +? '[HorribleSubs] Maria the Virgin Witch - 01 [720p].mkv' +: episode: 1 + release_group: HorribleSubs + screen_size: 720p + title: Maria the Virgin Witch + +? '[ISLAND]One_Piece_679_[VOSTFR]_[V1]_[8bit]_[720p]_[EB7838FC].mp4' +: crc32: EB7838FC + episode: 679 + release_group: ISLAND + screen_size: 720p + title: One Piece + subtitle_language: fr + color_depth: 8-bit + version: 1 + +? '[ISLAND]One_Piece_679_[VOSTFR]_[8bit]_[720p]_[EB7838FC].mp4' +: crc32: EB7838FC + episode: 679 + release_group: ISLAND + screen_size: 720p + title: One Piece + subtitle_language: fr + color_depth: 8-bit + +? '[Kaerizaki-Fansub]_One_Piece_679_[VOSTFR][HD_1280x720].mp4' +: episode: 679 + other: HD + release_group: Kaerizaki-Fansub + screen_size: 720p + title: One Piece + subtitle_language: fr + +? '[Kaerizaki-Fansub]_One_Piece_679_[VOSTFR][FANSUB][HD_1280x720].mp4' +: episode: 679 + other: [Fan Subtitled, HD] + release_group: Kaerizaki-Fansub + screen_size: 720p + title: One Piece + subtitle_language: fr + +? '[Kaerizaki-Fansub]_One_Piece_681_[VOSTFR][HD_1280x720]_V2.mp4' +: episode: 681 + other: HD + release_group: Kaerizaki-Fansub + screen_size: 720p + title: One Piece + subtitle_language: fr + version: 2 + +? '[Kaerizaki-Fansub] High School DxD New 04 VOSTFR HD (1280x720) V2.mp4' +: episode: 4 + other: HD + release_group: Kaerizaki-Fansub + screen_size: 720p + title: High School DxD New + subtitle_language: fr + version: 2 + +? '[Kaerizaki-Fansub] One Piece 603 VOSTFR PS VITA (960x544) V2.mp4' +: episode: 603 + release_group: Kaerizaki-Fansub + other: PS Vita + screen_size: 960x544 + title: One Piece + subtitle_language: fr + version: 2 + +? '[Group Name] Show Name.13' +: episode: 13 + release_group: Group Name + title: Show Name + +? '[Group Name] Show Name - 13' +: episode: 13 + release_group: Group Name + title: Show Name + +? '[Group Name] Show Name 13' +: episode: 13 + release_group: Group Name + title: Show Name + +# [Group Name] Show Name.13-14 +# [Group Name] Show Name - 13-14 +# Show Name 13-14 + +? '[Stratos-Subs]_Infinite_Stratos_-_12_(1280x720_H.264_AAC)_[379759DB]' +: audio_codec: AAC + crc32: 379759DB + episode: 12 + release_group: Stratos-Subs + screen_size: 720p + title: Infinite Stratos + video_codec: H.264 + +# [ShinBunBu-Subs] Bleach - 02-03 (CX 1280x720 x264 AAC) + +? '[SGKK] Bleach 312v1 [720p/MKV]' +: episode: 312 + release_group: SGKK + screen_size: 720p + title: Bleach + version: 1 + +? '[Ayako]_Infinite_Stratos_-_IS_-_07_[H264][720p][EB7838FC]' +: crc32: EB7838FC + episode: 7 + release_group: Ayako + screen_size: 720p + title: Infinite Stratos + video_codec: H.264 + +? '[Ayako] Infinite Stratos - IS - 07v2 [H264][720p][44419534]' +: crc32: '44419534' + episode: 7 + release_group: Ayako + screen_size: 720p + title: Infinite Stratos + video_codec: H.264 + version: 2 + +? '[Ayako-Shikkaku] Oniichan no Koto Nanka Zenzen Suki Janain Dakara ne - 10 [LQ][h264][720p] [8853B21C]' +: crc32: 8853B21C + episode: 10 + release_group: Ayako-Shikkaku + screen_size: 720p + title: Oniichan no Koto Nanka Zenzen Suki Janain Dakara ne + video_codec: H.264 + +? Bleach - s16e03-04 - 313-314 +? Bleach.s16e03-04.313-314-GROUP +? Bleach s16e03e04 313-314 +: title: Bleach + season: 16 + episode: [3, 4] + absolute_episode: [313, 314] + +? Bleach - 313-314 +: options: -E + episode: [313, 314] + title: Bleach + +? '[ShinBunBu-Subs] Bleach - 02-03 (CX 1280x720 x264 AAC)' +: audio_codec: AAC + episode: [2, 3] + release_group: ShinBunBu-Subs + screen_size: 720p + title: Bleach + video_codec: H.264 + +? 003. Show Name - Ep Name.avi +: episode: 3 + title: Show Name + episode_title: Ep Name + +? 003-004. Show Name - Ep Name.avi +: episode: [3, 4] + title: Show Name + episode_title: Ep Name + +? One Piece - 102 +: episode: 2 + season: 1 + title: One Piece + +? "[ACX]_Wolf's_Spirit_001.mkv" +: episode: 1 + release_group: ACX + title: "Wolf's Spirit" + +? Project.Runway.S14E00.and.S14E01.(Eng.Subs).SDTV.x264-[2Maverick].mp4 +: episode: [0, 1] + source: TV + release_group: 2Maverick + season: 14 + title: Project Runway + subtitle_language: en + video_codec: H.264 + +? '[Hatsuyuki-Kaitou]_Fairy_Tail_2_-_16-20_[720p][10bit].torrent' +: episode: [16, 17, 18, 19, 20] + release_group: Hatsuyuki-Kaitou + screen_size: 720p + title: Fairy Tail 2 + color_depth: 10-bit + +? '[Hatsuyuki-Kaitou]_Fairy_Tail_2_-_16-20_(191-195)_[720p][10bit].torrent' +: episode: [16, 17, 18, 19, 20] + absolute_episode: [191, 192, 193, 194, 195] + release_group: Hatsuyuki-Kaitou + screen_size: 720p + title: Fairy Tail 2 + +? "Looney Tunes 1940x01 Porky's Last Stand.mkv" +: episode: 1 + season: 1940 + title: Looney Tunes + episode_title: Porky's Last Stand + year: 1940 + +? The.Good.Wife.S06E01.E10.720p.WEB-DL.DD5.1.H.264-CtrlHD/The.Good.Wife.S06E09.Trust.Issues.720p.WEB-DL.DD5.1.H.264-CtrlHD.mkv +: audio_channels: '5.1' + audio_codec: Dolby Digital + episode: 9 + source: Web + release_group: CtrlHD + screen_size: 720p + season: 6 + title: The Good Wife + episode_title: Trust Issues + video_codec: H.264 + +? Fear the Walking Dead - 01x02 - So Close, Yet So Far.REPACK-KILLERS.French.C.updated.Addic7ed.com.mkv +: episode: 2 + language: fr + other: Proper + proper_count: 1 + season: 1 + title: Fear the Walking Dead + episode_title: So Close, Yet So Far + +? Fear the Walking Dead - 01x02 - En Close, Yet En Far.REPACK-KILLERS.French.C.updated.Addic7ed.com.mkv +: episode: 2 + language: fr + other: Proper + proper_count: 1 + season: 1 + title: Fear the Walking Dead + episode_title: En Close, Yet En Far + +? /av/unsorted/The.Daily.Show.2015.07.22.Jake.Gyllenhaal.720p.HDTV.x264-BATV.mkv +: date: 2015-07-22 + source: HDTV + release_group: BATV + screen_size: 720p + title: The Daily Show + episode_title: Jake Gyllenhaal + video_codec: H.264 + +? "[7.1.7.8.5] Foo Bar - 11 (H.264) [5235532D].mkv" +: episode: 11 + +? my 720p show S01E02 +: options: -T "my 720p show" + title: my 720p show + season: 1 + episode: 2 + +? my 720p show S01E02 720p +: options: -T "my 720p show" + title: my 720p show + season: 1 + episode: 2 + screen_size: 720p + +? -my 720p show S01E02 +: options: -T "re:my \d+p show" + screen_size: 720p + +? Show S01E02 +: options: -T "The Show" + title: Show + season: 1 + episode: 2 + +? Foo's & Bars (2009) S01E01 720p XviD-2HD[AOEU] +: episode: 1 + release_group: 2HD[AOEU] + screen_size: 720p + season: 1 + title: Foo's & Bars + video_codec: Xvid + year: 2009 + +? Date.Series.10-11-2008.XViD +: date: 2008-11-10 + title: Date + video_codec: Xvid + +? Scrubs/SEASON-06/Scrubs.S06E09.My.Perspective.DVDRip.XviD-WAT/scrubs.s06e09.dvdrip.xvid-wat.avi +: container: avi + episode: 9 + episode_title: My Perspective + source: DVD + other: Rip + release_group: WAT + season: 6 + title: Scrubs + video_codec: Xvid + +? '[PuyaSubs!] Digimon Adventure tri - 01 [720p][F9967949].mkv' +: container: mkv + crc32: F9967949 + episode: 1 + release_group: PuyaSubs! + screen_size: 720p + title: Digimon Adventure tri + +? Sherlock.S01.720p.BluRay.x264-AVCHD +: source: Blu-ray + screen_size: 720p + season: 1 + title: Sherlock + video_codec: H.264 + +? Running.Wild.With.Bear.Grylls.S02E07.Michael.B.Jordan.PROPER.HDTV.x264-W4F.avi +: container: avi + episode: 7 + episode_title: Michael B Jordan + source: HDTV + other: Proper + proper_count: 1 + release_group: W4F + season: 2 + title: Running Wild With Bear Grylls + video_codec: H.264 + +? Homeland.S05E11.Our.Man.in.Damascus.German.Sub.720p.HDTV.x264.iNTERNAL-BaCKToRG +: episode: 11 + episode_title: Our Man in Damascus + source: HDTV + other: Internal + release_group: BaCKToRG + screen_size: 720p + season: 5 + subtitle_language: de + title: Homeland + type: episode + video_codec: H.264 + +? Breaking.Bad.S01E01.2008.BluRay.VC1.1080P.5.1.WMV-NOVO +: title: Breaking Bad + season: 1 + episode: 1 + year: 2008 + source: Blu-ray + screen_size: 1080p + audio_channels: '5.1' + container: WMV + release_group: NOVO + type: episode + +? Cosmos.A.Space.Time.Odyssey.S01E02.HDTV.x264.PROPER-LOL +: title: Cosmos A Space Time Odyssey + season: 1 + episode: 2 + source: HDTV + video_codec: H.264 + other: Proper + proper_count: 1 + release_group: LOL + type: episode + +? Fear.The.Walking.Dead.S02E01.HDTV.x264.AAC.MP4-k3n +: title: Fear The Walking Dead + season: 2 + episode: 1 + source: HDTV + video_codec: H.264 + audio_codec: AAC + container: mp4 + release_group: k3n + type: episode + +? Elementary.S01E01.Pilot.DVDSCR.x264.PREAiR-NoGRP +: title: Elementary + season: 1 + episode: 1 + episode_details: Pilot + episode_title: Pilot + source: DVD + video_codec: H.264 + other: [Screener, Preair] + release_group: NoGRP + type: episode + +? Once.Upon.a.Time.S05E19.HDTV.x264.REPACK-LOL[ettv] +: title: Once Upon a Time + season: 5 + episode: 19 + source: HDTV + video_codec: H.264 + other: Proper + proper_count: 1 + release_group: LOL[ettv] + type: episode + +? Show.Name.S01E03.WEB-DL.x264.HUN-nIk +: title: Show Name + season: 1 + episode: 3 + source: Web + video_codec: H.264 + language: hu + release_group: nIk + type: episode + +? Game.of.Thrones.S6.Ep5.X265.Dolby.2.0.KTM3.mp4 +: audio_channels: '2.0' + audio_codec: Dolby Digital + container: mp4 + episode: 5 + release_group: KTM3 + season: 6 + title: Game of Thrones + type: episode + video_codec: H.265 + +? Fargo.-.Season.1.-.720p.BluRay.-.x264.-.ShAaNiG +: source: Blu-ray + release_group: ShAaNiG + screen_size: 720p + season: 1 + title: Fargo + type: episode + video_codec: H.264 + +? Show.Name.S02E02.Episode.Title.1080p.WEB-DL.x264.5.1Ch.-.Group +: audio_channels: '5.1' + episode: 2 + episode_title: Episode Title + source: Web + release_group: Group + screen_size: 1080p + season: 2 + title: Show Name + type: episode + video_codec: H.264 + +? Breaking.Bad.S01E01.2008.BluRay.VC1.1080P.5.1.WMV-NOVO +: audio_channels: '5.1' + container: wmv + episode: 1 + source: Blu-ray + release_group: NOVO + screen_size: 1080p + season: 1 + title: Breaking Bad + type: episode + year: 2008 + +? Cosmos.A.Space.Time.Odyssey.S01E02.HDTV.x264.PROPER-LOL +: episode: 2 + source: HDTV + other: Proper + proper_count: 1 + release_group: LOL + season: 1 + title: Cosmos A Space Time Odyssey + type: episode + video_codec: H.264 + +? Elementary.S01E01.Pilot.DVDSCR.x264.PREAiR-NoGRP +: episode: 1 + episode_details: Pilot + episode_title: Pilot + source: DVD + other: + - Screener + - Preair + release_group: NoGRP + season: 1 + title: Elementary + type: episode + video_codec: H.264 + +? Fear.The.Walking.Dead.S02E01.HDTV.x264.AAC.MP4-k3n.mp4 +: audio_codec: AAC + container: mp4 + episode: 1 + source: HDTV + release_group: k3n + season: 2 + title: Fear The Walking Dead + type: episode + video_codec: H.264 + +? Game.of.Thrones.S03.1080p.BluRay.DTS-HD.MA.5.1.AVC.REMUX-FraMeSToR +: audio_channels: '5.1' + audio_codec: DTS-HD + audio_profile: Master Audio + source: Blu-ray + other: Remux + release_group: FraMeSToR + screen_size: 1080p + season: 3 + title: Game of Thrones + type: episode + +? Show.Name.S01E02.HDTV.x264.NL-subs-ABC +: episode: 2 + source: HDTV + release_group: ABC + season: 1 + subtitle_language: nl + title: Show Name + type: episode + video_codec: H.264 + +? Friends.S01-S10.COMPLETE.720p.BluRay.x264-PtM +: source: Blu-ray + other: Complete + release_group: PtM + screen_size: 720p + season: # Should it be [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ? + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + title: Friends + type: episode + video_codec: H.264 + +? Duck.Dynasty.S02E07.Streik.German.DOKU.DL.WS.DVDRiP.x264-CDP +: episode: 7 + episode_title: Streik German + source: DVD + language: mul + other: [Documentary, Widescreen, Rip] + release_group: CDP + season: 2 + title: Duck Dynasty + type: episode + video_codec: H.264 + +? Family.Guy.S13E14.JOLO.German.AC3D.DL.720p.WebHD.x264-CDD +: audio_codec: Dolby Digital + episode: 14 + episode_title: JOLO German + source: Web + language: mul + release_group: CDD + screen_size: 720p + season: 13 + title: Family Guy + type: episode + video_codec: H.264 + +? How.I.Met.Your.Mother.COMPLETE.SERIES.DVDRip.XviD-AR +: options: -L en -C us + source: DVD + other: [Complete, Rip] + release_group: AR + title: How I Met Your Mother + type: movie # Should be episode + video_codec: Xvid + +? Show Name The Complete Seasons 1 to 5 720p BluRay x265 HEVC-SUJAIDR[UTR] +: source: Blu-ray + other: Complete + release_group: SUJAIDR[UTR] + screen_size: 720p + season: + - 1 + - 2 + - 3 + - 4 + - 5 + title: Show Name + type: episode + video_codec: H.265 + +? Fear.the.Walking.Dead.-.Season.2.epi.02.XviD.Eng.Ac3-5.1.sub.ita.eng.iCV-MIRCrew +: options: -t episode + audio_channels: '5.1' + audio_codec: Dolby Digital + episode: 2 + episode_title: epi + language: en + release_group: iCV-MIRCrew + season: 2 + subtitle_language: it + title: Fear the Walking Dead + type: episode + video_codec: Xvid + +? Game.Of.Thrones.S06E04.720p.PROPER.HDTV.x264-HDD +: episode: 4 + source: HDTV + other: Proper + proper_count: 1 + release_group: HDD + screen_size: 720p + season: 6 + title: Game Of Thrones + type: episode + video_codec: H.264 + +? Marvels.Daredevil.S02E04.WEBRip.x264-NF69.mkv +: container: mkv + episode: 4 + source: Web + other: Rip + release_group: NF69 + season: 2 + title: Marvels Daredevil + type: episode + video_codec: H.264 + +? The.Walking.Dead.S06E01.FRENCH.1080p.WEB-DL.DD5.1.HEVC.x265-GOLF68 +: audio_channels: '5.1' + audio_codec: Dolby Digital + episode: 1 + source: Web + language: fr + release_group: GOLF68 + screen_size: 1080p + season: 6 + title: The Walking Dead + type: episode + video_codec: H.265 + +? American.Crime.S01E03.FASTSUB.VOSTFR.720p.HDTV.x264-F4ST +: episode: 3 + source: HDTV + other: Fast Subtitled + release_group: F4ST + screen_size: 720p + season: 1 + subtitle_language: fr + title: American Crime + type: episode + video_codec: H.264 + +? Gotham.S02E12.FASTSUB.VOSTFR.HDTV.X264-F4ST3R +: episode: 12 + source: HDTV + other: Fast Subtitled + release_group: F4ST3R + season: 2 + subtitle_language: fr + title: Gotham + type: episode + video_codec: H.264 + +# WEBRip + LD +? Australian.Story.2016.05.23.Into.The.Fog.of.War.Part.1.360p.LDTV.WEBRIP.[MPup] +: title: Australian Story + date: 2016-05-23 + episode_title: Into The Fog of War + part: 1 + screen_size: 360p + other: [Low Definition, Rip] + source: Web + release_group: MPup + type: episode + +# AHDTV +? Show.Name.S04E06.FRENCH.AHDTV.XviD +: title: Show Name + season: 4 + episode: 6 + language: fr + source: Analog HDTV + video_codec: Xvid + type: episode + +# WEBDLRip +? Show.Name.s06e14.WEBDLRip.-qqss44.avi +: title: Show Name + season: 6 + episode: 14 + source: Web + other: Rip + release_group: qqss44 + container: avi + type: episode + +# WEBCap +? Steven.Universe.S03E06.Steven.Floats.720p.WEBCap.x264-SRS +: title: Steven Universe + season: 3 + episode: 6 + episode_title: Steven Floats + screen_size: 720p + source: Web + other: Rip + video_codec: H.264 + release_group: SRS + type: episode + +# DSR +? Show.Name.S05E09.Some.Episode.Title.WS.DSR.x264-[NY2] +: title: Show Name + season: 5 + episode: 9 + episode_title: Some Episode Title + other: Widescreen + source: Satellite + video_codec: H.264 + release_group: NY2 + type: episode + +# DSRip +? Squidbillies.S04E05.WS.DSRip.XviD-aAF +: title: Squidbillies + season: 4 + episode: 5 + other: [Widescreen, Rip] + source: Satellite + video_codec: Xvid + release_group: aAF + type: episode + + +? /series/The.B*.B*.T*.S10E01.1080p.HDTV.X264-DIMENSION[rarbg]/The.B*.B*.T*.S10E01.1080p.HDTV.X264-DIMENSION.mkv +: container: mkv + episode: 1 + source: HDTV + release_group: DIMENSION + screen_size: 1080p + season: 10 + title: The B B T + type: episode + video_codec: H.264 + +? '[Y-F] Very long Show Name Here - 03 Vostfr HD 8bits' +: release_group: Y-F + title: Very long Show Name Here + episode: 3 + subtitle_language: fr + other: HD + color_depth: 8-bit + type: episode + +? '[.www.site.com.].-.Snooze.and.Go.Sleep.S03E02.1080p.HEVC.x265-MeGusta' +: episode: 2 + release_group: MeGusta + screen_size: 1080p + season: 3 + title: Snooze and Go Sleep + type: episode + video_codec: H.265 + website: www.site.com + +? Show.Name.S01.720p.HDTV.DD5.1.x264-Group/show.name.0106.720p-group.mkv +: title: Show Name + season: 1 + screen_size: 720p + source: HDTV + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: Group + episode: 6 + container: mkv + type: episode + + +? Coupling Season 1 - 4 Complete DVDRip/Coupling Season 4/Coupling - (4x03) - Bed Time.mkv +: title: Coupling + other: [Complete, Rip] + source: DVD + season: 4 + episode: 3 + episode_title: Bed Time + container: mkv + type: episode + + +? Vice.News.Tonight.2016.10.10.1080p.HBO.WEBRip.AAC2.0.H.264-monkee +: title: Vice News Tonight + date: 2016-10-10 + screen_size: 1080p + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +? frasier.s8e6-768660.srt +: container: srt + episode: 6 + episode_title: '768660' + season: 8 + title: frasier + type: episode + +? Show.Name.S03E15.480p.177mb.Proper.HDTV.x264 +: title: Show Name + season: 3 + episode: 15 + screen_size: 480p + size: 177MB + other: Proper + proper_count: 1 + source: HDTV + video_codec: H.264 + type: episode + +? Show.Name.S03E15.480p.4.8GB.Proper.HDTV.x264 +: title: Show Name + season: 3 + episode: 15 + screen_size: 480p + size: 4.8GB + other: Proper + proper_count: 1 + source: HDTV + video_codec: H.264 + type: episode + +? Show.Name.S03.1.1TB.Proper.HDTV.x264 +: title: Show Name + season: 3 + size: 1.1TB + other: Proper + proper_count: 1 + source: HDTV + video_codec: H.264 + type: episode + +? Some.Show.S02E14.1080p.HDTV.X264-reenc.GROUP +? Some.Show.S02E14.1080p.HDTV.X264-re-enc.GROUP +? Some.Show.S02E14.1080p.HDTV.X264-re-encoded.GROUP +? Some.Show.S02E14.1080p.HDTV.X264-reencoded.GROUP +: title: Some Show + season: 2 + episode: 14 + screen_size: 1080p + source: HDTV + video_codec: H.264 + other: Reencoded + release_group: GROUP + type: episode + +# DDP is DD+ +? Show.Name.2016.S01E01.2160p.AMZN.WEBRip.DDP5.1.x264-Group +: title: Show Name + year: 2016 + season: 1 + episode: 1 + screen_size: 2160p + streaming_service: Amazon Prime + source: Web + other: Rip + audio_codec: Dolby Digital Plus + audio_channels: '5.1' + video_codec: H.264 + release_group: Group + type: episode + +? Show Name S02e19 [Mux - H264 - Ita Aac] DLMux by UBi +: title: Show Name + season: 2 + episode: 19 + video_codec: H.264 + language: it + audio_codec: AAC + source: Web + other: Mux + release_group: UBi + type: episode + +? Show Name S01e10[Mux - 1080p - H264 - Ita Eng Ac3 - Sub Ita Eng]DLMux By GiuseppeTnT Littlelinx +: title: Show Name + season: 1 + episode: 10 + screen_size: 1080p + video_codec: H.264 + language: [it, en] + source: Web + other: Mux + audio_codec: Dolby Digital + subtitle_language: [it, en] + release_group: GiuseppeTnT Littlelinx + type: episode + +? Show Name S04e07-08 [H264 - Ita Aac] HDTVMux by Group +: title: Show Name + season: 4 + episode: [7, 8] + video_codec: H.264 + language: it + audio_codec: AAC + source: HDTV + other: Mux + release_group: Group + type: episode + +? Show Name 3x18 Un Tuffo Nel Passato ITA HDTVMux x264 Group +: title: Show Name + season: 3 + episode: 18 + episode_title: Un Tuffo Nel Passato + language: it + source: HDTV + other: Mux + video_codec: H.264 + release_group: Group + type: episode + +? Show.Name.S03.1080p.BlurayMUX.AVC.DTS-HD.MA +: title: Show Name + season: 3 + screen_size: 1080p + source: Blu-ray + other: Mux + video_codec: H.264 + audio_codec: DTS-HD + audio_profile: Master Audio + type: episode + +? Show.Name.-.07.(2016).[RH].[English.Dubbed][WEBRip]..[HD.1080p] +: options: -t episode + episode: 7 + source: Web + other: Rip + language: en + other: [HD, Rip] + screen_size: 1080p + title: Show Name + type: episode + year: 2016 + +? Show.Name.-.476-479.(2007).[HorribleSubs][WEBRip]..[HD.720p] +: options: -t episode + episode: + - 476 + - 477 + - 478 + - 479 + source: Web + other: [Rip, HD] + release_group: HorribleSubs + screen_size: 720p + title: Show Name + type: episode + year: 2007 + +? /11.22.63/Season 1/11.22.63.106.hdtv-abc +: options: -T 11.22.63 + title: 11.22.63 + season: 1 + episode: 6 + source: HDTV + release_group: abc + type: episode + +? Proof.2015.S01E10.1080p.WEB-DL.DD5.1.H.264-KINGS.mkv +: title: Proof + season: 1 + episode: 10 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: KINGS + container: mkv + type: episode + +# Hardcoded subtitles +? Show.Name.S06E16.HC.SWESUB.HDTV.x264 +: title: Show Name + season: 6 + episode: 16 + other: Hardcoded Subtitles + source: HDTV + video_codec: H.264 + subtitle_language: sv + type: episode + +? From [ WWW.TORRENTING.COM ] - White.Rabbit.Project.S01E08.1080p.NF.WEBRip.DD5.1.x264-ViSUM/White.Rabbit.Project.S01E08.1080p.NF.WEBRip.DD5.1.x264-ViSUM.mkv +: title: White Rabbit Project + website: WWW.TORRENTING.COM + season: 1 + episode: 8 + screen_size: 1080p + streaming_service: Netflix + source: Web + other: Rip + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: ViSUM + container: mkv + type: episode + +? /tv/Daniel Tiger's Neighborhood/S02E06 - Playtime Is Different.mp4 +: season: 2 + episode: 6 + title: Daniel Tiger's Neighborhood + episode_title: Playtime Is Different + container: mp4 + type: episode + +? Zoo.S02E05.1080p.WEB-DL.DD5.1.H.264.HKD/160725_02.mkv +: title: Zoo + season: 2 + episode: 5 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: HKD + container: mkv + type: episode + +? We.Bare.Bears.S01E14.Brother.Up.1080p.WEB-DL.AAC2.0.H.264-TVSmash/mxNMuJWeO7PUWCMEwqKSsS6D8Vs9S6V3PHD.mkv +: title: We Bare Bears + season: 1 + episode: 14 + episode_title: Brother Up + screen_size: 1080p + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: TVSmash + container: mkv + type: episode + +? Beyond.S01E02.Tempus.Fugit.720p.FREE.WEBRip.AAC2.0.x264-BTW/gNWDXow11s7E0X7GTDrZ.mkv +: title: Beyond + season: 1 + episode: 2 + episode_title: Tempus Fugit + screen_size: 720p + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTW + container: mkv + type: episode + +? Bones.S12E02.The.Brain.In.The.Bot.1080p.WEB-DL.DD5.1.H.264-R2D2/161219_06.mkv +: title: Bones + season: 12 + episode: 2 + episode_title: The Brain In The Bot + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: R2D2 + container: mkv + type: episode + +? The.Messengers.2015.S01E07.1080p.WEB-DL.DD5.1.H264.Nlsubs-Q/QoQ-sbuSLN.462.H.1.5DD.LD-BEW.p0801.70E10S.5102.sregnesseM.ehT.mkv +: title: The Messengers + year: 2015 + season: 1 + episode: 7 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + subtitle_language: nl + release_group: Q + container: mkv + type: episode + +? /Finding.Carter.S02E01.Love.the.Way.You.Lie.1080p.WEB-DL.AAC2.0.H.264-NL/LN-462.H.0.2CAA.LD-BEW.p0801.eiL.uoY.yaW.eht.evoL.10E20S.retraC.gnidniF.mkv +: title: Finding Carter + season: 2 + episode: 1 + episode_title: Love the Way You Lie + screen_size: 1080p + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: NL + container: mkv + type: episode + +? Mr.Robot.S02E12.1080p.WEB-DL.DD5.1-NL.Subs-Het.Robot.Team.OYM/sbuS LN-1.5DD LD-BEW p0801 21E20S toboR .rM.mkv +: title: Mr Robot + season: 2 + episode: 12 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + release_group: Het.Robot.Team.OYM + type: episode + +? Show.Name.-.Temporada.1.720p.HDTV.x264[Cap.102]SPANISH.AUDIO-NEWPCT +? /Show Name/Season 01/Show.Name.-.Temporada.1.720p.HDTV.x264[Cap.102]SPANISH.AUDIO-NEWPCT +? /Show Name/Temporada 01/Show.Name.-.Temporada.1.720p.HDTV.x264[Cap.102]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 1 + episode: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct +? Show Name - Temporada 4 [HDTV][Cap.408][Espanol Castellano] +? Show Name - Temporada 4 [HDTV][Cap.408][Español Castellano] +: title: Show Name + season: 4 + episode: 8 + source: HDTV + language: ca + type: episode + +# newpct +? -Show Name - Temporada 4 [HDTV][Cap.408][Espanol Castellano] +? -Show Name - Temporada 4 [HDTV][Cap.408][Español Castellano] +: release_group: Castellano + +# newpct +? Show.Name.-.Temporada1.[HDTV][Cap.105][Español.Castellano] +: title: Show Name + source: HDTV + season: 1 + episode: 5 + language: ca + type: episode + +# newpct +? Show.Name.-.Temporada1.[HDTV][Cap.105][Español] +: title: Show Name + source: HDTV + season: 1 + episode: 5 + language: es + type: episode + +# newpct - season and episode with range: +? Show.Name.-.Temporada.1.720p.HDTV.x264[Cap.102_104]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 1 + episode: [2, 3, 4] + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct - season and episode (2 digit season) +? Show.Name.-.Temporada.15.720p.HDTV.x264[Cap.1503]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 15 + episode: 3 + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct - season and episode (2 digit season with range) +? Show.Name.-.Temporada.15.720p.HDTV.x264[Cap.1503_1506]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 15 + episode: [3, 4, 5, 6] + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct - season and episode: +? Show.Name.-.Temp.1.720p.HDTV.x264[Cap.102]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 1 + episode: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct - season and episode: +? Show.Name.-.Tem.1.720p.HDTV.x264[Cap.102]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 1 + episode: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + type: episode + +# newpct - season and episode: +? Show.Name.-.Tem.1.720p.HDTV.x264[Cap.112_114.Final]SPANISH.AUDIO-NEWPCT +: title: Show Name + season: 1 + episode: [12, 13, 14] + screen_size: 720p + source: HDTV + video_codec: H.264 + language: es + release_group: NEWPCT + episode_details: Final + type: episode + +? Mastercook Italia - Stagione 6 (2016) 720p ep13 spyro.mkv +: title: Mastercook Italia + season: 6 + episode: 13 + year: 2016 + screen_size: 720p + episode_title: spyro + container: mkv + type: episode + +? Mastercook Italia - Stagione 6 (2016) 720p Episodio 13 spyro.mkv +: title: Mastercook Italia + season: 6 + year: 2016 + screen_size: 720p + episode: 13 + episode_title: spyro + container: mkv + type: episode + +# Italian releases +? Show Name 3x18 Un Tuffo Nel Passato ITA HDTVMux x264 NovaRip +: title: Show Name + season: 3 + episode: 18 + episode_title: Un Tuffo Nel Passato + language: it + source: HDTV + other: Mux + video_codec: H.264 + release_group: NovaRip + type: episode + +# Italian releases +? Show Name 3x18 Un Tuffo Nel Passato ITA HDTVMux x264 NovaRip +: title: Show Name + season: 3 + episode: 18 + episode_title: Un Tuffo Nel Passato + language: it + source: HDTV + other: Mux + video_codec: H.264 + release_group: NovaRip + type: episode + +# Subbed: No language hint +? Show.Name.S06E03.1080p.HDTV.Legendado +: subtitle_language: und + +# Subbed: No language hint +? Show.Name.S01E09.Subbed.1080p.BluRay.x264-RRH +: title: Show Name + season: 1 + episode: 9 + subtitle_language: und + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: RRH + type: episode + +# Legendado PT-BR +? Show.Name.S06E05.1080p.WEBRip.Legendado.PT-BR +? Show.Name.S06E05.1080p.WEBRip.Legendas.PT-BR +? Show.Name.S06E05.1080p.WEBRip.Legenda.PT-BR +: title: Show Name + season: 6 + episode: 5 + screen_size: 1080p + source: Web + other: Rip + subtitle_language: pt-BR + type: episode + +? Show.Name.S01E07.Super, Title.WEB-DL 720p.br.srt +: title: Show Name + season: 1 + episode: 7 + episode_title: Super, Title + source: Web + screen_size: 720p + subtitle_language: pt-BR + container: srt + type: episode + +? -Show.Name.S01E07.Super, Title.WEB-DL 720p.br.srt +: language: pt-BR + +# Legendado PT +? Show.Name.S06E05.1080p.WEBRip.Legendado.PT +: title: Show Name + season: 6 + episode: 5 + screen_size: 1080p + source: Web + other: Rip + subtitle_language: pt + type: episode + +? Show.Name.S05E01.SPANISH.SUBBED.720p.HDTV.x264-sPHD +: title: Show Name + season: 5 + episode: 1 + subtitle_language: spa + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: sPHD + type: episode + +? Show.Name.S01E01.German.Subbed.HDTV.XviD-ASAP +: title: Show Name + season: 1 + episode: 1 + subtitle_language: deu + source: HDTV + video_codec: Xvid + release_group: ASAP + type: episode + +? Show.Name.S04E21.Aint.Nothing.Like.the.Real.Thing.German.Custom.Subbed.720p.HDTV.x264.iNTERNAL-BaCKToRG +: title: Show Name + season: 4 + episode: 21 + episode_title: Aint Nothing Like the Real Thing + subtitle_language: deu + screen_size: 720p + source: HDTV + video_codec: H.264 + type: episode + +? Show.Name.S01.Season.Complet.WEBRiP.Ro.Subbed.TM +: title: Show Name + season: 1 + other: [Complete, Rip] + source: Web + subtitle_language: ro + type: episode + +? Show.Name.(2013).Season.3.-.Eng.Soft.Subtitles.720p.WEBRip.x264.[MKV,AC3,5.1].Ehhhh +: title: Show Name + year: 2013 + season: 3 + subtitle_language: en + screen_size: 720p + source: Web + other: Rip + video_codec: H.264 + container: mkv + audio_codec: Dolby Digital + audio_channels: '5.1' + release_group: Ehhhh + type: episode + +# Dublado +? Show.Name.S02E03.720p.HDTV.x264-Belex.-.Dual.Audio.-.Dublado +: title: Show Name + season: 2 + episode: 3 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: Belex + other: Dual Audio + language: und + type: episode + +? Show.Name.S06E10.1080p.WEB-DL.DUAL.[Dublado].RK +: title: Show Name + season: 6 + episode: 10 + screen_size: 1080p + source: Web + other: Dual Audio + language: und + release_group: RK + type: episode + +? Show.Name.S06E12.720p.WEB-DL.Dual.Audio.Dublado +: title: Show Name + season: 6 + episode: 12 + screen_size: 720p + source: Web + other: Dual Audio + language: und + type: episode + +? Show.Name.S05E07.720p.DUBLADO.HDTV.x264-0SEC-pia.mkv +: title: Show Name + season: 5 + episode: 7 + screen_size: 720p + language: und + source: HDTV + video_codec: H.264 + release_group: 0SEC-pia + container: mkv + type: episode + +? Show.Name.S02E07.Shiva.AC3.Dubbed.WEBRip.x264 +: title: Show Name + season: 2 + episode: 7 + episode_title: Shiva + audio_codec: Dolby Digital + language: und + source: Web + other: Rip + video_codec: H.264 + type: episode + +# Legendas +? Show.Name.S05.1080p.BluRay.x264-Belex.-.Dual.Audio.+.Legendas +: title: Show Name + season: 5 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: Belex + other: Dual Audio + subtitle_language: und + type: episode + +# Legendas +? Show.Name.S05.1080p.BluRay.x264-Belex.-.Dual.Audio.+.Legendas +: title: Show Name + season: 5 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: Belex + other: Dual Audio + subtitle_language: und + type: episode + +# Subtitulado +? Show.Name.S01E03.HDTV.Subtitulado.Esp.SC +? Show.Name.S01E03.HDTV.Subtitulado.Espanol.SC +? Show.Name.S01E03.HDTV.Subtitulado.Español.SC +: title: Show Name + season: 1 + episode: 3 + source: HDTV + subtitle_language: es + release_group: SC + type: episode + +# Subtitles/Subbed +? Show.Name.S02E08.720p.WEB-DL.Subtitles +? Show.Name.S02E08.Subbed.720p.WEB-DL +: title: Show Name + season: 2 + episode: 8 + screen_size: 720p + source: Web + subtitle_language: und + type: episode + +# Dubbed +? Show.Name.s01e01.german.Dubbed +: title: Show Name + season: 1 + episode: 1 + language: de + type: episode + +? Show.Name.S06E05.Das.Toor.German.AC3.Dubbed.HDTV.German +: title: Show Name + season: 6 + episode: 5 + language: de + audio_codec: Dolby Digital + source: HDTV + type: episode + +? Show.Name.S01E01.Savage.Season.GERMAN.DUBBED.WS.HDTVRip.x264-TVP +: title: Show Name + season: 1 + episode: 1 + episode_title: Savage Season + language: de + other: [Widescreen, Rip] + source: HDTV + video_codec: H.264 + release_group: TVP + type: episode + +# Dubbed +? "[AnimeRG].Show.Name.-.03.[Eng.Dubbed].[720p].[WEB-DL].[JRR]" +: title: Show Name + episode: 3 + language: en + screen_size: 720p + source: Web + release_group: JRR + type: episode + +# Dubbed +? "[RH].Show.Name.-.03.[English.Dubbed].[1080p]" +: title: Show Name + episode: 3 + language: en + screen_size: 1080p + release_group: RH + type: episode + +# Hebsubs +? Show.Name.S05E05.HDTV.XviD-AFG.HebSubs +: title: Show Name + season: 5 + episode: 5 + source: HDTV + video_codec: Xvid + release_group: AFG + subtitle_language: he + type: episode + +? Show Name - S02E31 - Episode 55 (720p.HDTV) +: title: Show Name + season: 2 + episode: 31 + episode_title: Episode 55 + screen_size: 720p + source: HDTV + type: episode + +# Scenario: Removing invalid season and episode matches. Correct episode_title match +? Show.Name.S02E06.eps2.4.m4ster-s1ave.aes.1080p.AMZN.WEBRip.DD5.1.x264-GROUP +: title: Show Name + season: 2 + episode: 6 + episode_title: eps2 4 m4ster-s1ave aes + screen_size: 1080p + streaming_service: Amazon Prime + source: Web + other: Rip + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: GROUP + type: episode + +? Show.Name.S01E05.3xpl0its.wmv.720p.WEBdl.EN-SUB.x264-[MULVAcoded].mkv +: title: Show Name + season: 1 + episode: 5 + episode_title: 3xpl0its + screen_size: 720p + source: Web + subtitle_language: en + video_codec: H.264 + type: episode + +# Regression: S4L release group detected as season 4 +# https://github.com/guessit-io/guessit/issues/352 +? Show Name S01E06 DVD-RIP x264-S4L +: title: Show Name + season: 1 + episode: 6 + source: DVD + video_codec: H.264 + release_group: S4L + type: episode + +# Corner case with only date and 720p +? The.Show.Name.2016.05.18.720.HDTV.x264-GROUP.VTV +: title: The Show Name + date: 2016-05-18 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: GROUP.VTV + type: episode + +# Corner case with only date and 720p +? -The.Show.Name.2016.05.18.720.HDTV.x264-GROUP.VTV +: season: 7 + episode: 20 + +# https://github.com/guessit-io/guessit/issues/308 (conflict with screen size) +? "[SuperGroup].Show.Name.-.06.[720.Hi10p][1F5578AC]" +: title: Show Name + episode: 6 + screen_size: 720p + color_depth: 10-bit + crc32: 1F5578AC + release_group: SuperGroup + type: episode + +# https://github.com/guessit-io/guessit/issues/308 (conflict with screen size) +? "[SuperGroup].Show.Name.-.06.[1080.Hi10p][1F5578AC]" +: title: Show Name + episode: 6 + screen_size: 1080p + color_depth: 10-bit + crc32: 1F5578AC + release_group: SuperGroup + type: episode + +? "[MK-Pn8].Dimension.W.-.05.[720p][Hi10][Dual][TV-Dub][EDA6E7F1]" +: options: -C us -L und + release_group: MK-Pn8 + title: Dimension W + episode: 5 + screen_size: 720p + color_depth: 10-bit + other: Dual Audio + source: TV + language: und + crc32: EDA6E7F1 + type: episode + +? "[Zero-Raws].Show.Name.493-498.&.500-507.(CX.1280x720.VFR.x264.AAC)" +: release_group: Zero-Raws + title: Show Name + episode: [493, 494, 495, 496, 497, 498, 500, 501, 502, 503, 504, 505, 506, 507] + screen_size: 720p + subtitle_language: fr + video_codec: H.264 + audio_codec: AAC + type: episode + +# NetflixUHD +? Show.Name.S01E06.NetflixUHD +: title: Show Name + season: 1 + episode: 6 + streaming_service: Netflix + other: Ultra HD + type: episode + +? Show.Name.S04E13.FINAL.MULTI.DD51.2160p.NetflixUHDRip.x265-TVS +: title: Show Name + season: 4 + episode: 13 + episode_details: Final + language: mul + audio_codec: Dolby Digital + audio_channels: '5.1' + screen_size: 2160p + streaming_service: Netflix + source: Ultra HDTV + other: Rip + video_codec: H.265 + release_group: TVS + type: episode + +? Show.Name.S06E11.Of.Late.I.Think.of.Rosewood.iTunesHD.x264 +: title: Show Name + season: 6 + episode: 11 + episode_title: Of Late I Think of Rosewood + streaming_service: iTunes + other: HD + video_codec: H.264 + type: episode + +? Show.Name.S01.720p.iTunes.h264-Group +: title: Show Name + season: 1 + screen_size: 720p + streaming_service: iTunes + video_codec: H.264 + release_group: Group + type: episode + +? Show.Name.1x01.eps1.0.hellofriend.(HDiTunes.Ac3.Esp).(2015).By.Malaguita.avi +: title: Show Name + season: 1 + episode: 1 + episode_title: eps1 0 hellofriend + other: HD + streaming_service: iTunes + audio_codec: Dolby Digital + language: spa + year: 2015 + container: avi + type: episode + +? "[Hanamaru&LoliHouse] The Dragon Dentist - 01 [WebRip 1920x1080 HEVC-yuv420p10 AAC].mkv" +: release_group: Hanamaru&LoliHouse + title: The Dragon Dentist + episode: 1 + source: Web + other: Rip + screen_size: 1080p + video_codec: H.265 + color_depth: 10-bit + audio_codec: AAC + container: mkv + type: episode + +? Show Name - Season 1 Episode 50 +: title: Show Name + season: 1 + episode: 50 + type: episode + +? Vikings.Seizoen.4.1080p.Web.NLsubs +: title: Vikings + season: 4 + screen_size: 1080p + source: Web + subtitle_language: nl + type: episode + +? Star.Wars.Rebels.S01E01.Spark.of.Rebellion.ALTERNATE.CUT.HDTV.x264-W4F.mp4 +: title: Star Wars Rebels + season: 1 + episode: 1 + episode_title: Spark of Rebellion + edition: Alternative Cut + source: HDTV + video_codec: H.264 + release_group: W4F + container: mp4 + type: episode + +? DCs.Legends.of.Tomorrow.S02E12.HDTV.XviD-FUM +: title: DCs Legends of Tomorrow + season: 2 + episode: 12 + source: HDTV + video_codec: Xvid + release_group: FUM + type: episode + +? DC's Legends of Tomorrow 2016 - S02E02 +: title: DC's Legends of Tomorrow + year: 2016 + season: 2 + episode: 2 + type: episode + +? Broadchurch.S01.DIRFIX.720p.BluRay.x264-SHORTBREHD +: title: Broadchurch + season: 1 + other: Proper + screen_size: 720p + source: Blu-ray + video_codec: H.264 + release_group: SHORTBREHD + proper_count: 1 + type: episode + +? Simply Red - 2016-07-08 Montreux Jazz Festival 720p +: title: Simply Red + date: 2016-07-08 + episode_title: Montreux Jazz Festival + screen_size: 720p + type: episode + +? Ridiculousness.S07E14.iNTERNAL.HDTV.x264-YesTV +: title: Ridiculousness + season: 7 + episode: 14 + other: Internal + source: HDTV + video_codec: H.264 + release_group: YesTV + type: episode + +? Stephen.Colbert.2016.05.25.James.McAvoy.iNTERNAL.XviD-AFG +: title: Stephen Colbert + date: 2016-05-25 + episode_title: James McAvoy + other: Internal + video_codec: Xvid + release_group: AFG + type: episode + +? The.100.S01E13.iNTERNAL.READNFO.720p.HDTV.x264-2HD +: title: The 100 + season: 1 + episode: 13 + other: [Internal, Read NFO] + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: 2HD + type: episode + +? The.100.S01E13.READ.NFO.720p.HDTV.x264-2HD +: title: The 100 + season: 1 + episode: 13 + other: Read NFO + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: 2HD + type: episode + +? Dr.Ken.S01E21.SAMPLEFIX.720p.HDTV.x264-SVA +: title: Dr Ken + season: 1 + episode: 21 + other: Proper + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: SVA + type: episode + +? Rick and Morty Season 1 [UNCENSORED] [BDRip] [1080p] [HEVC] +: title: Rick and Morty + season: 1 + edition: Uncensored + other: Rip + source: Blu-ray + screen_size: 1080p + video_codec: H.265 + type: episode + +? 12.Monkeys.S01E01.LiMiTED.FRENCH.1080p.WEB-DL.H264-AUTHORiTY +: title: 12 Monkeys + season: 1 + episode: 1 + edition: Limited + language: french + screen_size: 1080p + source: Web + video_codec: H.264 + release_group: AUTHORiTY + type: episode + +? Undateable.2014.S03E05.West.Feed.HDTV.x264-2HD +: title: Undateable + year: 2014 + season: 3 + episode: 5 + other: West Coast Feed + source: HDTV + video_codec: H.264 + release_group: 2HD + type: episode + +? Undateable.2014.S02E07-E08.Live.Episode.West.Coast.Feed.HDTV.x264-2HD +: title: Undateable + year: 2014 + season: 2 + episode: [7, 8] + other: West Coast Feed + source: HDTV + video_codec: H.264 + release_group: 2HD + type: episode + +? Undateable.S03E01-E02.LIVE.EAST.FEED.720p.HDTV.x264-KILLERS +: title: Undateable + season: 3 + episode: [1, 2] + other: East Coast Feed + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: KILLERS + type: episode + +? Undateable.2014.S02E07.Live.Episode.East.Coast.Feed.HDTV.x264-2HD +: title: Undateable + year: 2014 + season: 2 + episode: 7 + other: East Coast Feed + source: HDTV + video_codec: H.264 + release_group: 2HD + type: episode + +? Undateable.2014.S02E07.East.Coast.Feed.720p.WEB-DL.DD5.1.H.264-NTb +: title: Undateable + year: 2014 + season: 2 + episode: 7 + other: East Coast Feed + screen_size: 720p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: NTb + type: episode + +? True Detective S02E04 720p HDTV x264-0SEC [GloDLS].mkv +: title: True Detective + season: 2 + episode: 4 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: 0SEC [GloDLS] + container: mkv + type: episode + +? Anthony.Bourdain.Parts.Unknown.S09E01.Los.Angeles.720p.HDTV.x264-MiNDTHEGAP +: title: Anthony Bourdain Parts Unknown + season: 9 + episode: 1 + episode_title: Los Angeles + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: MiNDTHEGAP + type: episode + +? -feud.s01e05.and.the.winner.is.(the.oscars.of.1963).720p.amzn.webrip.dd5.1.x264-casstudio.mkv +: year: 1963 + +? feud.s01e05.and.the.winner.is.(the.oscars.of.1963).720p.amzn.webrip.dd5.1.x264-casstudio.mkv +: title: feud + season: 1 + episode: 5 + episode_title: and the winner is + screen_size: 720p + streaming_service: Amazon Prime + source: Web + other: Rip + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: casstudio + container: mkv + type: episode + +? Adventure.Time.S08E16.Elements.Part.1.Skyhooks.720p.WEB-DL.AAC2.0.H.264-RTN.mkv +: title: Adventure Time + season: 8 + episode: 16 + episode_title: Elements Part 1 Skyhooks + screen_size: 720p + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: RTN + container: mkv + type: episode + +? D:\TV\SITCOMS (CLASSIC)\That '70s Show\Season 07\That '70s Show - S07E22 - 2000 Light Years from Home.mkv +: title: That '70s Show + season: 7 + episode: 22 + episode_title: 2000 Light Years from Home + container: mkv + type: episode + +? Show.Name.S02E01.Super.Title.720p.WEB-DL.DD5.1.H.264-ABC.nzb +: title: Show Name + season: 2 + episode: 1 + episode_title: Super Title + screen_size: 720p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: ABC + container: nzb + type: episode + +? "[SGKK] Bleach 312v1 [720p/mkv]-Group.mkv" +: title: Bleach + episode: 312 + version: 1 + screen_size: 720p + release_group: Group + container: mkv + type: episode + +? The.Expanse.S02E08.720p.WEBRip.x264.EAC3-KiNGS.mkv +: title: The Expanse + season: 2 + episode: 8 + screen_size: 720p + source: Web + other: Rip + video_codec: H.264 + audio_codec: Dolby Digital Plus + release_group: KiNGS + container: mkv + type: episode + +? Series_name.2005.211.episode.title.avi +: title: Series name + year: 2005 + season: 2 + episode: 11 + episode_title: episode title + container: avi + type: episode + +? the.flash.2014.208.hdtv-lol[ettv].mkv +: title: the flash + year: 2014 + season: 2 + episode: 8 + source: HDTV + release_group: lol[ettv] + container: mkv + type: episode + +? "[Despair-Paradise].Kono.Subarashii.Sekai.ni.Shukufuku.wo!.2.-..09.vostfr.FHD" +: release_group: Despair-Paradise + title: Kono Subarashii Sekai ni Shukufuku wo! 2 + episode: 9 + subtitle_language: fr + other: Full HD + type: episode + +? Whose Line is it anyway/Season 01/Whose.Line.is.it.Anyway.US.S13E01.720p.WEB.x264-TBS.mkv +: title: Whose Line is it Anyway + season: 13 + episode: 1 + country: US + screen_size: 720p + source: Web + video_codec: H.264 + release_group: TBS + container: mkv + type: episode + +? Planet.Earth.II.S01.2160p.UHD.BluRay.HDR.DTS-HD.MA5.1.x265-ULTRAHDCLUB +: title: Planet Earth II + season: 1 + screen_size: 2160p + source: Ultra HD Blu-ray + other: HDR10 + audio_codec: DTS-HD + audio_profile: Master Audio + audio_channels: '5.1' + video_codec: H.265 + release_group: ULTRAHDCLUB + type: episode + +? Reizen.Waes.S03.FLEMISH.1080p.HDTV.MP2.H.264-NOGRP/Reizen.Waes.S03E05.China.PART1.FLEMISH.1080p.HDTV.MP2.H.264-NOGRP.mkv +: title: Reizen Waes + season: 3 + episode: 5 + part: 1 + language: nl-BE + screen_size: 1080p + source: HDTV + video_codec: H.264 + release_group: NOGRP + container: mkv + type: episode + +? "/folder/Marvels.Agent.Carter.S02E05.The.Atomic.Job.1080p.WEB-DL.DD5.1.H264-Coo7[rartv]/Marvel's.Agent.Carter.S02E05.The.Atomic.Job.1080p.WEB-DL.DD5.1.H.264-Coo7.mkv" +: title: Marvel's Agent Carter + season: 2 + episode: 5 + episode_title: The Atomic Job + release_group: Coo7 + type: episode + +? My.Name.Is.Earl.S01-S04.DVDRip.XviD-AR +: title: My Name Is Earl + season: [1, 2, 3, 4] + source: DVD + other: Rip + video_codec: Xvid + release_group: AR + type: episode + +? American.Dad.S01E01.Pilot.DVDRip.x264-CS +: title: American Dad + season: 1 + episode: 1 + episode_details: Pilot + source: DVD + other: Rip + video_codec: H.264 + release_group: CS + type: episode + +? Black.Sails.S01E01.HDTV.XviD.HebSubs-DR +: title: Black Sails + season: 1 + episode: 1 + source: HDTV + video_codec: Xvid + subtitle_language: he + release_group: DR + type: episode + +? The.West.Wing.S04E06.Game.On.720p.WEB-DL.AAC2.0.H.264-MC +: title: The West Wing + season: 4 + episode: 6 + episode_title: Game On + screen_size: 720p + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: MC + type: episode + +? 12.Monkeys.S02E05.1080p.WEB-DL.DD5.1.H.264-NA +: title: 12 Monkeys + season: 2 + episode: 5 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: NA + type: episode + +? Fear.the.Walking.Dead.S03E07.1080p.AMZN.WEBRip.DD5.1.x264-VLAD[rarbg]/Fear.the.Walking.Dead.S03E07.1080p.AMZN.WEB-DL.DD+5.1.H.264-VLAD.mkv +: title: Fear the Walking Dead + season: 3 + episode: 7 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital Plus + audio_channels: '5.1' + video_codec: H.264 + release_group: VLAD + container: mkv + type: episode + +? American.Crime.S01E02.1080p.WEB-DL.DD5.1.H.264-NL +: title: American Crime + season: 1 + episode: 2 + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: NL + type: episode + +? Better.Call.Saul.S02.720p.HDTV.x264-TL +: title: Better Call Saul + season: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: TL + type: episode + +? 60.Minutes.2008.12.14.HDTV.XviD-YT +: options: -T '60 Minutes' + title: 60 Minutes + date: 2008-12-14 + source: HDTV + video_codec: Xvid + release_group: YT + type: episode + +? Storm.Chasers.Season.1 +: title: Storm Chasers + season: 1 + type: episode + +? Faking.It.2014.S03E08.720p.HDTV.x264-AVS +: title: Faking It + year: 2014 + season: 3 + episode: 8 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: AVS + type: episode + +? /series/Marvel's Agents of S.H.I.E.L.D/Season 4/Marvels.Agents.of.S.H.I.E.L.D.S04E01.The.Ghost.1080p.WEB-DL.DD5.1.H.264-AG.mkv +: title: Marvels Agents of S.H.I.E.L.D. + season: 4 + episode: 1 + episode_title: The Ghost + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: AG + container: mkv + type: episode + +? "[FASubs & TTF] Inuyasha - 099 [DVD] [B15AA1AC].mkv" +: release_group: FASubs & TTF + title: Inuyasha + episode: 99 + source: DVD + crc32: B15AA1AC + container: mkv + type: episode + +? Show.Name.S01E03.PL.SUBBED.480p.WEBRiP.x264 +: title: Show Name + season: 1 + episode: 3 + subtitle_language: pl + screen_size: 480p + source: Web + other: Rip + video_codec: H.264 + type: episode + +? Show.Name.s10e15(233).480p.BDRip-AVC.Ukr.hurtom +: title: Show Name + season: 10 + episode: 15 + screen_size: 480p + source: Blu-ray + other: Rip + video_codec: H.264 + language: uk + release_group: hurtom + type: episode + +? Goof.Troop.1x24.Waste.Makes.Haste.720p.HDTV.x264.CZ-SDTV +: title: Goof Troop + season: 1 + episode: 24 + episode_title: Waste Makes Haste + screen_size: 720p + source: HDTV + video_codec: H.264 + language: cs + release_group: SDTV + type: episode + +? Marvels.Daredevil.S02E11.German.DL.DUBBED.2160p.WebUHD.x264-UHDTV +: title: Marvels Daredevil + season: 2 + episode: 11 + language: [de, mul] + screen_size: 2160p + source: Web + video_codec: H.264 + release_group: UHDTV + type: episode + +? BBC The Story of China 1 of 6 - Ancestors CC HDTV x264 AC3 2.0 720p mkv +: title: BBC The Story of China + episode: 1 + episode_count: 6 + episode_title: Ancestors + source: HDTV + video_codec: H.264 + audio_codec: Dolby Digital + audio_channels: '2.0' + screen_size: 720p + container: mkv + type: episode + +? Duck.Dynasty.S09E04.Drone.Survivor.720p.AE.WEBRip.AAC2.0.H264-BTW[rartv] +: title: Duck Dynasty + season: 9 + episode: 4 + episode_title: Drone Survivor + screen_size: 720p + streaming_service: A&E + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTW[rartv] + type: episode + +? Mr.Selfridge.S04E03.720p.WEB-DL.AAC2.0.H264-MS[rartv] +: title: Mr Selfridge + season: 4 + episode: 3 + screen_size: 720p + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: MS[rartv] + type: episode + +? Second.Chance.S01E02.One.More.Notch.1080p.WEB-DL.DD5.1.H264-SC[rartv] +: title: Second Chance + season: 1 + episode: 2 + episode_title: One More Notch + screen_size: 1080p + source: Web + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: rartv + type: episode + +? Total.Divas.S05E01.720p.HDTV.AAC2.0.H.264-SC-SDH +: title: Total Divas + season: 5 + episode: 1 + screen_size: 720p + source: HDTV + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: SDH + type: episode + +? Marvel's Jessica Jones (2015) s01e09 - AKA Sin Bin.mkv +: title: Marvel's Jessica Jones + season: 1 + episode: 9 + episode_title: AKA Sin Bin + container: mkv + type: episode + +? Hotel.Hell.S01E01.720p.DD5.1.448kbps-ALANiS +: title: Hotel Hell + season: 1 + episode: 1 + screen_size: 720p + audio_codec: Dolby Digital + audio_channels: '5.1' + audio_bit_rate: 448Kbps + release_group: ALANiS + type: episode + +? Greys.Anatomy.S07D1.NTSC.DVDR-ToF +: title: Greys Anatomy + season: 7 + disc: 1 + other: NTSC + source: DVD + release_group: ToF + type: episode + +? Greys.Anatomy.S07D1.NTSC.DVDR-ToF +: title: Greys Anatomy + season: 7 + disc: 1 + other: NTSC + source: DVD + release_group: ToF + type: episode + +? Greys.Anatomy.S07D1-3&5.NTSC.DVDR-ToF +: title: Greys Anatomy + season: 7 + disc: [1, 2, 3, 5] + other: NTSC + source: DVD + release_group: ToF + type: episode + +? El.Principe.2014.S01D01.SPANiSH.COMPLETE.BLURAY-COJONUDO +: title: El Principe + year: 2014 + season: 1 + disc: 1 + language: spa + other: Complete + source: Blu-ray + release_group: COJONUDO + type: episode + +? The Simpsons - Season 2 Complete [DVDRIP VP7 KEGGERMAN +: title: The Simpsons + season: 2 + other: [Complete, Rip] + source: DVD + video_codec: VP7 + release_group: KEGGERMAN + type: episode + +? Barney & Friends_ Easy as ABC (Season 9_ Episode 15)_VP8_Vorbis_360p.webm +: title: Barney & Friends Easy as ABC + season: 9 + episode: 15 + video_codec: VP8 + audio_codec: Vorbis + screen_size: 360p + container: webm + type: episode + +? Victoria.S01.1080p.BluRay.HEVC.DTSMA.LPCM.PGS-OZM +: title: Victoria + season: 1 + screen_size: 1080p + source: Blu-ray + video_codec: H.265 + audio_codec: [DTS-HD, LPCM] + audio_profile: Master Audio + # Does it worth to add subtitle_format? Such rare case + # subtitle_format: PGS + # release_group: OZM + type: episode + +? The.Prisoners.S01E03.1080p.DM.AAC2.0.x264-BTN +: title: The Prisoners + season: 1 + episode: 3 + screen_size: 1080p + source: Digital Master + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTN + type: episode + +? Panorama.S2013E25.Broken.by.Battle.1080p.DM.AAC2.0.x264-BTN +: title: Panorama + season: 2013 + episode: 25 + episode_title: Broken by Battle + screen_size: 1080p + source: Digital Master + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTN + type: episode + +? Our.World.S2014E11.Chinas.Model.Army.720p.DM.AAC2.0.x264-BTN +: title: Our World + season: 2014 + episode: 11 + episode_title: Chinas Model Army + screen_size: 720p + source: Digital Master + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTN + type: episode + +? Storyville.S2016E08.My.Nazi.Legacy.1080p.DM.x264-BTN +: title: Storyville + season: 2016 + episode: 8 + episode_title: My Nazi Legacy + screen_size: 1080p + source: Digital Master + video_codec: H.264 + release_group: BTN + type: episode + +? Comedians.in.Cars.Getting.Coffee.S07E01.1080p.DM.FLAC2.0.x264-NTb +: title: Comedians in Cars Getting Coffee + season: 7 + episode: 1 + screen_size: 1080p + source: Digital Master + audio_codec: FLAC + audio_channels: '2.0' + video_codec: H.264 + release_group: NTb + type: episode + +? "[SomeGroup-Fansub]_Show_Name_727_[VOSTFR][HD_1280x720]" +: release_group: SomeGroup-Fansub + title: Show Name + episode: 727 + subtitle_language: fr + other: HD + screen_size: 720p + type: episode + +? "[GROUP]Show_Name_726_[VOSTFR]_[V1]_[8bit]_[720p]_[2F7B3FA2]" +: release_group: GROUP + title: Show Name + episode: 726 + subtitle_language: fr + version: 1 + color_depth: 8-bit + screen_size: 720p + crc32: 2F7B3FA2 + type: episode + +? Show Name 445 VOSTFR par Fansub-Resistance (1280*720) - version MQ +: title: Show Name + episode: 445 + subtitle_language: fr + screen_size: 720p + type: episode + +? Anime Show Episode 159 v2 [VOSTFR][720p][AAC].mp4 +: title: Anime Show + episode: 159 + version: 2 + subtitle_language: fr + screen_size: 720p + audio_codec: AAC + container: mp4 + type: episode + +? "[Group] Anime Super Episode 161 [VOSTFR][720p].mp4" +: release_group: Group + title: Anime Super + episode: 161 + subtitle_language: fr + screen_size: 720p + container: mp4 + type: episode + +? Anime Show Episode 59 v2 [VOSTFR][720p][AAC].mp4 +: title: Anime Show + episode: 59 + version: 2 + subtitle_language: fr + screen_size: 720p + audio_codec: AAC + container: mp4 + type: episode + +? Show.Name.-.476-479.(2007).[HorribleSubs][WEBRip]..[HD.720p] +: title: Show Name + episode: [476, 477, 478, 479] + year: 2007 + release_group: HorribleSubs + source: Web + other: [Rip, HD] + screen_size: 720p + type: episode + +? Show Name - 722 [HD_1280x720].mp4 +: title: Show Name + episode: 722 + other: HD + screen_size: 720p + container: mp4 + type: episode + +? Show!.Name.2.-.10.(2016).[HorribleSubs][WEBRip]..[HD.720p] +: title: Show! Name 2 + episode: 10 + year: 2016 + release_group: HorribleSubs + source: Web + other: [Rip, HD] + screen_size: 720p + type: episode + +? 'C:\folder\[GROUP]_An_Anime_Show_100_-_10_[1080p]_mkv' +: options: -T 'An Anime Show 100' + release_group: GROUP + title: An Anime Show 100 + episode: 10 + screen_size: 1080p + container: mkv + type: episode + +? "[Group].Show.Name!.Super!!.-.05.[720p][AAC].mp4" +: release_group: Group + title: Show Name! Super!! + episode: 5 + screen_size: 720p + audio_codec: AAC + container: mp4 + type: episode + +? "[GROUP].Mobile.Suit.Gundam.Unicorn.RE.0096.-.14.[720p].mkv" +: options: -T 'Mobile Suit Gundam Unicorn RE 0096' + release_group: GROUP + title: Mobile Suit Gundam Unicorn RE 0096 + episode: 14 + screen_size: 720p + container: mkv + type: episode + +? Show.Name.-.Other Name.-.02.(1280x720.HEVC.AAC) +: title: Show Name + alternative_title: Other Name + episode: 2 + screen_size: 720p + video_codec: H.265 + audio_codec: AAC + type: episode + +? "[GroupName].Show.Name.-.02.5.(Special).[BD.1080p]" +: release_group: GroupName + title: Show Name + episode: 2 + episode_details: Special + screen_size: 1080p + source: Blu-ray + type: episode + +? "[Group].Show.Name.2.The.Big.Show.-.11.[1080p]" +: title: Show Name 2 The Big Show + episode: 11 + screen_size: 1080p + type: episode + +? "[SuperGroup].Show.Name.-.Still.Name.-.11.[1080p]" +: release_group: SuperGroup + title: Show Name + alternative_title: Still Name + episode: 11 + screen_size: 1080p + type: episode + +? "[SuperGroup].Show.Name.-.462" +: release_group: SuperGroup + title: Show Name + episode: 462 + type: episode + +? Show.Name.10.720p +: title: Show Name + episode: 10 + screen_size: 720p + type: episode + +? "[Group].Show.Name.G2.-.19.[1080p]" +: release_group: Group + title: Show Name G2 + episode: 19 + screen_size: 1080p + type: episode + +? "[Group].Show.Name.S2.-.19.[1080p]" +? /Show.Name.S2/[Group].Show.Name.S2.-.19.[1080p] +? /Show Name S2/[Group].Show.Name.S2.-.19.[1080p] +: options: -T 'Show Name S2' + release_group: Group + title: Show Name S2 + episode: 19 + screen_size: 1080p + type: episode + +? "[ABC]_Show_Name_001.mkv" +: release_group: ABC + title: Show Name + episode: 1 + container: mkv + type: episode + +? 003-005. Show Name - Ep Name.mkv +: episode: [3, 4, 5] + title: Show Name + episode_title: Ep Name + container: mkv + type: episode + +? 003. Show Name - Ep Name.mkv +: episode: 3 + title: Show Name + episode_title: Ep Name + container: mkv + type: episode + +? 165.Show Name.s08e014 +: absolute_episode: 165 + title: Show Name + season: 8 + episode: 14 + type: episode + +? Show Name - 16x03-05 - 313-315 +? Show.Name.16x03-05.313-315-GROUP +? Show Name 16x03-05 313-315 +? Show Name - 313-315 - s16e03-05 +? Show.Name.313-315.s16e03-05 +? Show Name 313-315 s16e03-05 +: title: Show Name + absolute_episode: [313, 314, 315] + season: 16 + episode: [3, 4, 5] + type: episode + +? Show Name 13-16 +: title: Show Name + episode: [13, 14, 15, 16] + type: episode + +? Show Name 804 vostfr HD +: options: --episode-prefer-number + title: Show Name + episode: 804 + subtitle_language: fr + other: HD + type: episode + +? "[Doki] Re Zero kara Hajimeru Isekai Seikatsu - 01 1920x1080 Hi10P BD FLAC [7F64383D].mkv" +: release_group: Doki + title: Re Zero kara Hajimeru Isekai Seikatsu + episode: 1 + screen_size: 1080p + aspect_ratio: 1.778 + video_profile: High 10 + color_depth: 10-bit + source: Blu-ray + audio_codec: FLAC + crc32: 7F64383D + container: mkv + type: episode + +? Shark Tank (AU) - S02E01 - HDTV-720p.mkv +: title: Shark Tank + country: AU + season: 2 + episode: 1 + source: HDTV + screen_size: 720p + container: mkv + type: episode + +? "[HorribleSubs] Garo - Vanishing Line - 01 [1080p].mkv" +: release_group: HorribleSubs + title: Garo + alternative_title: Vanishing Line + episode: 1 + screen_size: 1080p + container: mkv + type: episode + +? "[HorribleSubs] Yowamushi Pedal - Glory Line - 01 [1080p].mkv" +: release_group: HorribleSubs + title: Yowamushi Pedal + alternative_title: Glory Line + episode: 1 + screen_size: 1080p + container: mkv + type: episode + +? c:\Temp\autosubliminal\completed\2 Broke Girls\Season 01\2 Broke Girls - S01E01 - HDTV-720p Proper - x264 AC3 - IMMERSE - [2011-09-19].mkv +: title: 2 Broke Girls + season: 1 + episode: 1 + source: HDTV + screen_size: 720p + other: Proper + video_codec: H.264 + audio_codec: Dolby Digital + release_group: IMMERSE + date: 2011-09-19 + container: mkv + type: episode + +? c:\Temp\postprocessing\Marvels.Agents.of.S.H.I.E.L.D.s01e02.0.8.4.720p.WEB.DL.mkv +: title: Marvels Agents of S.H.I.E.L.D. + season: 1 + episode: 2 + episode_title: 0.8.4. + screen_size: 720p + source: Web + container: mkv + type: episode + +? Mind.Field.S02E06.The.Power.of.Suggestion.1440p.H264.WEBDL.Subtitles +: title: Mind Field + season: 2 + episode: 6 + episode_title: The Power of Suggestion + screen_size: 1440p + video_codec: H.264 + source: Web + subtitle_language: und + type: episode + +? The Power of Suggestion - Mind Field S2 (Ep 6) (1440p_24fps_H264-384kbit_AAC 6Ch).mp4 +: title: The Power of Suggestion + alternative_title: Mind Field + season: 2 + episode: 6 + screen_size: 1440p + frame_rate: 24fps + video_codec: H.264 + audio_bit_rate: 384Kbps + audio_codec: AAC + audio_channels: '5.1' + container: mp4 + type: episode + +? Mind.Field.S02E06.The.Power.of.Suggestion.1440p.H264.WEBDL.Subtitles/The Power of Suggestion - Mind Field S2 (Ep 6) (1440p_24fps_H264-384kbit_AAC 6Ch).mp4 +: season: 2 + episode: 6 + title: The Power of Suggestion + alternative_title: Mind Field + screen_size: 1440p + frame_rate: 24fps + video_codec: H.264 + source: Web + subtitle_language: und + audio_bit_rate: 384Kbps + audio_codec: AAC + audio_channels: '5.1' + container: mp4 + type: episode + +? Mind.Field.S02E06.The.Power.of.Suggestion.1440p.H264.WEBDL.Subtitles/The Power of Suggestion - Mind Field S2 (Ep 6) (English).srt +: title: Mind Field + season: 2 + episode: 6 + episode_title: The Power of Suggestion + screen_size: 1440p + video_codec: H.264 + source: Web + subtitle_language: en + container: srt + type: episode + +? Mind.Field.S02E06.The.Power.of.Suggestion.1440p.H264.WEBDL.Subtitles/The Power of Suggestion - Mind Field S2 (Ep 6) (Korean).srt +: title: Mind Field + season: 2 + episode: 6 + episode_title: The Power of Suggestion + screen_size: 1440p + video_codec: H.264 + source: Web + subtitle_language: ko + container: srt + type: episode + +? '[HorribleSubs] Overlord II - 01 [1080p] 19.1mbits - 120fps.mkv' +: release_group: HorribleSubs + title: Overlord II + episode: 1 + screen_size: 1080p + video_bit_rate: 19.1Mbps + frame_rate: 120fps + container: mkv + type: episode + +? One Piece - 720 +: title: One Piece + season: 7 + episode: 20 + type: episode + +? foobar.213.avi +: options: -E + title: foobar + episode: 213 + container: avi + type: episode + +? FooBar - 360 368p-Grp +: options: -E + title: FooBar + episode: 360 + screen_size: 368p + release_group: Grp + type: episode + +? wwiis.most.daring.raids.s01e04.storming.mussolinis.island.1080p.web.h.264-edhd-sample.mkv +: title: wwiis most daring raids + season: 1 + episode: 4 + episode_title: storming mussolinis island + screen_size: 1080p + source: Web + video_codec: H.264 + release_group: edhd + other: Sample + container: mkv + type: episode + +? WWIIs.Most.Daring.Raids.S01E04.Storming.Mussolinis.Island.1080p.WEB.h264-EDHD/wwiis.most.daring.raids.s01e04.storming.mussolinis.island.1080p.web.h.264-edhd-sample.mkv +: title: wwiis most daring raids + season: 1 + episode: 4 + episode_title: Storming Mussolinis Island + screen_size: 1080p + source: Web + video_codec: H.264 + release_group: edhd + other: Sample + container: mkv + type: episode + +? dcs.legends.of.tomorrow.s02e01.1080p.bluray.x264-rovers.proof +: title: dcs legends of tomorrow + season: 2 + episode: 1 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: rovers + other: Proof + type: episode + +? dcs.legends.of.tomorrow.s02e01.720p.bluray.x264-demand.sample.mkv +: title: dcs legends of tomorrow + season: 2 + episode: 1 + screen_size: 720p + source: Blu-ray + video_codec: H.264 + release_group: demand + other: Sample + container: mkv + type: episode + +? Season 06/e01.1080p.bluray.x264-wavey-obfuscated.mkv +: season: 6 + episode: 1 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + title: wavey + other: Obfuscated + container: mkv + type: episode + +? Hells.Kitchen.US.S17E08.1080p.HEVC.x265-MeGusta-Obfuscated/c48db7d2aeb040e8a920a9fd6effcbf4.mkv +: title: Hells Kitchen + country: US + season: 17 + episode: 8 + screen_size: 1080p + video_codec: H.265 + release_group: MeGusta + other: Obfuscated + uuid: c48db7d2aeb040e8a920a9fd6effcbf4 + container: mkv + type: episode + +? Blue.Bloods.S08E09.1080p.HEVC.x265-MeGusta-Obfuscated/afaae96ae7a140e0981ced2a79221751.mkv +: title: Blue Bloods + season: 8 + episode: 9 + screen_size: 1080p + video_codec: H.265 + release_group: MeGusta + other: Obfuscated + container: mkv + type: episode + +? MacGyver.2016.S02E09.CD-ROM.and.Hoagie.Foil.1080p.AMZN.WEBRip.DDP5.1.x264-NTb-Scrambled/c329b27187d44a94b4a25b21502db552.mkv +: title: MacGyver + year: 2016 + season: 2 + episode: 9 + screen_size: 1080p + streaming_service: Amazon Prime + source: Web + other: [Rip, Obfuscated] + audio_codec: Dolby Digital Plus + audio_channels: '5.1' + video_codec: H.264 + release_group: NTb + uuid: c329b27187d44a94b4a25b21502db552 + container: mkv + type: episode + +? The.Late.Late.Show.with.James.Corden.2017.11.27.Armie.Hammer.Juno.Temple.Charlie.Puth.1080p.AMZN.WEB-DL.DDP2.0.H.264-monkee-Scrambled/42e7e8a48eb7454aaebebcf49705ce41.mkv +: title: The Late Late Show with James Corden + date: 2017-11-27 + episode_title: Armie Hammer Juno Temple Charlie Puth + screen_size: 1080p + streaming_service: Amazon Prime + source: Web + audio_codec: Dolby Digital Plus + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + other: Obfuscated + uuid: 42e7e8a48eb7454aaebebcf49705ce41 + container: mkv + type: episode + +? Educating Greater Manchester S01E07 720p HDTV x264-PLUTONiUM-AsRequested +: title: Educating Greater Manchester + season: 1 + episode: 7 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: PLUTONiUM + other: Repost + type: episode + +? Im A Celebrity Get Me Out Of Here S17E14 HDTV x264-PLUTONiUM-xpost +: title: Im A Celebrity Get Me Out Of Here + season: 17 + episode: 14 + source: HDTV + video_codec: H.264 + release_group: PLUTONiUM + other: Repost + type: episode + +? Tales S01E08 All I Need Method Man Featuring Mary J Blige 720p BET WEBRip AAC2 0 x264-RTN-xpost +: title: Tales + season: 1 + episode: 8 + episode_title: All I Need Method Man Featuring Mary J Blige + screen_size: 720p + source: Web + other: [Rip, Repost] + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: RTN + type: episode + +? This is Us S01E11 Herzensangelegenheiten German DL WS DVDRip x264-CDP-xpost +: options: --exclude country + title: This is Us + season: 1 + episode: 11 + episode_title: Herzensangelegenheiten + language: + - de + - mul + other: + - Widescreen + - Rip + - Repost + source: DVD + video_codec: H.264 + release_group: CDP + type: episode + +? The Girlfriend Experience S02E10 1080p WEB H264-STRiFE-postbot +: title: The Girlfriend Experience + season: 2 + episode: 10 + screen_size: 1080p + source: Web + video_codec: H.264 + release_group: STRiFE + other: Repost + type: episode + +? The.Girlfriend.Experience.S02E10.1080p.WEB.H264-STRiFE-postbot/90550c1adaf44c47b60d24f59603bb98.mkv +: title: The Girlfriend Experience + season: 2 + episode: 10 + screen_size: 1080p + source: Web + video_codec: H.264 + release_group: STRiFE + other: Repost + uuid: 90550c1adaf44c47b60d24f59603bb98 + container: mkv + type: episode + +? 24.S01E02.1080p.BluRay.REMUX.AVC.DD.2.0-EPSiLON-xpost/eb518eaf33f641a1a8c6e0973a67aec2.mkv +: title: '24' + season: 1 + episode: 2 + screen_size: 1080p + source: Blu-ray + other: [Remux, Repost] + video_codec: H.264 + audio_codec: Dolby Digital + audio_channels: '2.0' + release_group: EPSiLON + uuid: eb518eaf33f641a1a8c6e0973a67aec2 + container: mkv + type: episode + +? Educating.Greater.Manchester.S01E02.720p.HDTV.x264-PLUTONiUM-AsRequested/47fbcb2393aa4b5cbbb340d3173ca1a9.mkv +: title: Educating Greater Manchester + season: 1 + episode: 2 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: PLUTONiUM + other: Repost + uuid: 47fbcb2393aa4b5cbbb340d3173ca1a9 + container: mkv + type: episode + +? Stranger.Things.S02E05.Chapter.Five.Dig.Dug.720p.NF.WEBRip.DD5.1.x264-PSYPHER-AsRequested-Obfuscated +: title: Stranger Things + season: 2 + episode: 5 + episode_title: Chapter Five Dig Dug + screen_size: 720p + streaming_service: Netflix + source: Web + other: [Rip, Repost, Obfuscated] + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: PSYPHER + type: episode + +? Show.Name.-.Season.1.3.4-.Mp4.1080p +: title: Show Name + season: [1, 3, 4] + container: mp4 + screen_size: 1080p + type: episode diff --git a/libs/guessit/test/movies.yml b/libs/guessit/test/movies.yml new file mode 100644 index 000000000..33d5d189b --- /dev/null +++ b/libs/guessit/test/movies.yml @@ -0,0 +1,1721 @@ +? __default__ +: type: movie + +? Movies/Fear and Loathing in Las Vegas (1998)/Fear.and.Loathing.in.Las.Vegas.720p.HDDVD.DTS.x264-ESiR.mkv +: title: Fear and Loathing in Las Vegas + year: 1998 + screen_size: 720p + source: HD-DVD + audio_codec: DTS + video_codec: H.264 + container: mkv + release_group: ESiR + +? Movies/El Dia de la Bestia (1995)/El.dia.de.la.bestia.DVDrip.Spanish.DivX.by.Artik[SEDG].avi +: title: El Dia de la Bestia + year: 1995 + source: DVD + other: Rip + language: spanish + video_codec: DivX + release_group: Artik[SEDG] + container: avi + +? Movies/Dark City (1998)/Dark.City.(1998).DC.BDRip.720p.DTS.X264-CHD.mkv +: title: Dark City + year: 1998 + source: Blu-ray + other: Rip + screen_size: 720p + audio_codec: DTS + video_codec: H.264 + release_group: CHD + +? Movies/Sin City (BluRay) (2005)/Sin.City.2005.BDRip.720p.x264.AC3-SEPTiC.mkv +: title: Sin City + year: 2005 + source: Blu-ray + other: Rip + screen_size: 720p + video_codec: H.264 + audio_codec: Dolby Digital + release_group: SEPTiC + +? Movies/Borat (2006)/Borat.(2006).R5.PROPER.REPACK.DVDRip.XviD-PUKKA.avi +: title: Borat + year: 2006 + proper_count: 2 + source: DVD + other: [ Region 5, Proper, Rip ] + video_codec: Xvid + release_group: PUKKA + +? "[XCT].Le.Prestige.(The.Prestige).DVDRip.[x264.HP.He-Aac.{Fr-Eng}.St{Fr-Eng}.Chaps].mkv" +: title: Le Prestige + source: DVD + other: Rip + video_codec: H.264 + video_profile: High + audio_codec: AAC + audio_profile: High Efficiency + language: [ french, english ] + subtitle_language: [ french, english ] + release_group: Chaps + +? Battle Royale (2000)/Battle.Royale.(Batoru.Rowaiaru).(2000).(Special.Edition).CD1of2.DVDRiP.XviD-[ZeaL].avi +: title: Battle Royale + year: 2000 + edition: Special + cd: 1 + cd_count: 2 + source: DVD + other: Rip + video_codec: Xvid + release_group: ZeaL + +? Movies/Brazil (1985)/Brazil_Criterion_Edition_(1985).CD2.avi +: title: Brazil + edition: Criterion + year: 1985 + cd: 2 + +? Movies/Persepolis (2007)/[XCT] Persepolis [H264+Aac-128(Fr-Eng)+ST(Fr-Eng)+Ind].mkv +: title: Persepolis + year: 2007 + video_codec: H.264 + audio_codec: AAC + language: [ French, English ] + subtitle_language: [ French, English ] + release_group: Ind + +? Movies/Toy Story (1995)/Toy Story [HDTV 720p English-Spanish].mkv +: title: Toy Story + year: 1995 + source: HDTV + screen_size: 720p + language: [ english, spanish ] + +? Movies/Office Space (1999)/Office.Space.[Dual-DVDRip].[Spanish-English].[XviD-AC3-AC3].[by.Oswald].avi +: title: Office Space + year: 1999 + other: [Dual Audio, Rip] + source: DVD + language: [ english, spanish ] + video_codec: Xvid + audio_codec: Dolby Digital + +? Movies/Wild Zero (2000)/Wild.Zero.DVDivX-EPiC.avi +: title: Wild Zero + year: 2000 + video_codec: DivX + release_group: EPiC + +? movies/Baraka_Edition_Collector.avi +: title: Baraka + edition: Collector + +? Movies/Blade Runner (1982)/Blade.Runner.(1982).(Director's.Cut).CD1.DVDRip.XviD.AC3-WAF.avi +: title: Blade Runner + year: 1982 + edition: Director's Cut + cd: 1 + source: DVD + other: Rip + video_codec: Xvid + audio_codec: Dolby Digital + release_group: WAF + +? movies/American.The.Bill.Hicks.Story.2009.DVDRip.XviD-EPiSODE.[UsaBit.com]/UsaBit.com_esd-americanbh.avi +: title: American The Bill Hicks Story + year: 2009 + source: DVD + other: Rip + video_codec: Xvid + release_group: EPiSODE + website: UsaBit.com + +? movies/Charlie.And.Boots.DVDRip.XviD-TheWretched/wthd-cab.avi +: title: Charlie And Boots + source: DVD + other: Rip + video_codec: Xvid + release_group: TheWretched + +? movies/Steig Larsson Millenium Trilogy (2009) BRrip 720 AAC x264/(1)The Girl With The Dragon Tattoo (2009) BRrip 720 AAC x264.mkv +: title: The Girl With The Dragon Tattoo + #film_title: Steig Larsson Millenium Trilogy + #film: 1 + year: 2009 + source: Blu-ray + other: [Reencoded, Rip] + audio_codec: AAC + video_codec: H.264 + screen_size: 720p + +? movies/Greenberg.REPACK.LiMiTED.DVDRip.XviD-ARROW/arw-repack-greenberg.dvdrip.xvid.avi +: title: Greenberg + source: DVD + video_codec: Xvid + release_group: ARROW + other: [Proper, Rip] + edition: Limited + proper_count: 1 + +? Movies/Fr - Paris 2054, Renaissance (2005) - De Christian Volckman - (Film Divx Science Fiction Fantastique Thriller Policier N&B).avi +: title: Paris 2054, Renaissance + year: 2005 + language: french + video_codec: DivX + +? Movies/[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi +: title: Avida + year: 2006 + language: french + source: DVD + other: Rip + video_codec: Xvid + release_group: PROD + +? Movies/Alice in Wonderland DVDRip.XviD-DiAMOND/dmd-aw.avi +: title: Alice in Wonderland + source: DVD + other: Rip + video_codec: Xvid + release_group: DiAMOND + +? Movies/Ne.Le.Dis.A.Personne.Fr 2 cd/personnea_mp.avi +: title: Ne Le Dis A Personne + language: french + cd_count: 2 + +? Movies/Bunker Palace Hôtel (Enki Bilal) (1989)/Enki Bilal - Bunker Palace Hotel (Fr Vhs Rip).avi +: title: Bunker Palace Hôtel + year: 1989 + language: french + source: VHS + other: Rip + +? Movies/21 (2008)/21.(2008).DVDRip.x264.AC3-FtS.[sharethefiles.com].mkv +: title: "21" + year: 2008 + source: DVD + other: Rip + video_codec: H.264 + audio_codec: Dolby Digital + release_group: FtS + website: sharethefiles.com + +? Movies/9 (2009)/9.2009.Blu-ray.DTS.720p.x264.HDBRiSe.[sharethefiles.com].mkv +: title: "9" + year: 2009 + source: Blu-ray + audio_codec: DTS + screen_size: 720p + video_codec: H.264 + release_group: HDBRiSe + website: sharethefiles.com + +? Movies/Mamma.Mia.2008.DVDRip.AC3.XviD-CrazyTeam/Mamma.Mia.2008.DVDRip.AC3.XviD-CrazyTeam.avi +: title: Mamma Mia + year: 2008 + source: DVD + other: Rip + audio_codec: Dolby Digital + video_codec: Xvid + release_group: CrazyTeam + +? Movies/M.A.S.H. (1970)/MASH.(1970).[Divx.5.02][Dual-Subtitulos][DVDRip].ogm +: title: MASH + year: 1970 + video_codec: DivX + source: DVD + other: [Dual Audio, Rip] + +? Movies/The Doors (1991)/09.03.08.The.Doors.(1991).BDRip.720p.AC3.X264-HiS@SiLUHD-English.[sharethefiles.com].mkv +: title: The Doors + year: 1991 + date: 2008-03-09 + source: Blu-ray + other: Rip + screen_size: 720p + audio_codec: Dolby Digital + video_codec: H.264 + release_group: HiS@SiLUHD + language: english + website: sharethefiles.com + +? Movies/The Doors (1991)/08.03.09.The.Doors.(1991).BDRip.720p.AC3.X264-HiS@SiLUHD-English.[sharethefiles.com].mkv +: options: --date-year-first + title: The Doors + year: 1991 + date: 2008-03-09 + source: Blu-ray + other: Rip + screen_size: 720p + audio_codec: Dolby Digital + video_codec: H.264 + release_group: HiS@SiLUHD + language: english + website: sharethefiles.com + +? Movies/Ratatouille/video_ts-ratatouille.srt +: title: Ratatouille + source: DVD + +# Removing this one because 001 is guessed as an episode number. +# ? Movies/001 __ A classer/Fantomas se déchaine - Louis de Funès.avi +# : title: Fantomas se déchaine + +? Movies/Comme une Image (2004)/Comme.Une.Image.FRENCH.DVDRiP.XViD-NTK.par-www.divx-overnet.com.avi +: title: Comme une Image + year: 2004 + language: french + source: DVD + other: Rip + video_codec: Xvid + release_group: NTK + website: www.divx-overnet.com + +? Movies/Fantastic Mr Fox/Fantastic.Mr.Fox.2009.DVDRip.{x264+LC-AAC.5.1}{Fr-Eng}{Sub.Fr-Eng}-™.[sharethefiles.com].mkv +: title: Fantastic Mr Fox + year: 2009 + source: DVD + other: Rip + video_codec: H.264 + audio_codec: AAC + audio_profile: Low Complexity + audio_channels: "5.1" + language: [ french, english ] + subtitle_language: [ french, english ] + website: sharethefiles.com + +? Movies/Somewhere.2010.DVDRip.XviD-iLG/i-smwhr.avi +: title: Somewhere + year: 2010 + source: DVD + other: Rip + video_codec: Xvid + release_group: iLG + +? Movies/Moon_(2009).mkv +: title: Moon + year: 2009 + +? Movies/Moon_(2009)-x02-Making_Of.mkv +: title: Moon + year: 2009 + bonus: 2 + bonus_title: Making Of + +? movies/James_Bond-f17-Goldeneye.mkv +: title: Goldeneye + film_title: James Bond + film: 17 + + +? /movies/James_Bond-f21-Casino_Royale.mkv +: title: Casino Royale + film_title: James Bond + film: 21 + +? /movies/James_Bond-f21-Casino_Royale-x01-Becoming_Bond.mkv +: title: Casino Royale + film_title: James Bond + film: 21 + bonus: 1 + bonus_title: Becoming Bond + +? /movies/James_Bond-f21-Casino_Royale-x02-Stunts.mkv +: title: Casino Royale + film_title: James Bond + film: 21 + bonus: 2 + bonus_title: Stunts + +? OSS_117--Cairo,_Nest_of_Spies.mkv +: title: OSS 117 +# TODO: Implement subTitle for movies. + +? The Godfather Part 3.mkv +? The Godfather Part III.mkv +: title: The Godfather + part: 3 + +? Foobar Part VI.mkv +: title: Foobar + part: 6 + +? The_Insider-(1999)-x02-60_Minutes_Interview-1996.mp4 +: title: The Insider + year: 1999 + bonus: 2 + bonus_title: 60 Minutes Interview-1996 + +? Rush.._Beyond_The_Lighted_Stage-x09-Between_Sun_and_Moon-2002_Hartford.mkv +: title: Rush Beyond The Lighted Stage + bonus: 9 + bonus_title: Between Sun and Moon + year: 2002 + +? /public/uTorrent/Downloads Finished/Movies/Indiana.Jones.and.the.Temple.of.Doom.1984.HDTV.720p.x264.AC3.5.1-REDµX/Indiana.Jones.and.the.Temple.of.Doom.1984.HDTV.720p.x264.AC3.5.1-REDµX.mkv +: title: Indiana Jones and the Temple of Doom + year: 1984 + source: HDTV + screen_size: 720p + video_codec: H.264 + audio_codec: Dolby Digital + audio_channels: "5.1" + release_group: REDµX + +? The.Director’s.Notebook.2006.Blu-Ray.x264.DXVA.720p.AC3-de[42].mkv +: title: The Director’s Notebook + year: 2006 + source: Blu-ray + video_codec: H.264 + video_api: DXVA + screen_size: 720p + audio_codec: Dolby Digital + release_group: de[42] + + +? Movies/Cosmopolis.2012.LiMiTED.720p.BluRay.x264-AN0NYM0US[bb]/ano-cosmo.720p.mkv +: title: Cosmopolis + year: 2012 + screen_size: 720p + video_codec: H.264 + release_group: AN0NYM0US[bb] + source: Blu-ray + edition: Limited + +? movies/La Science des Rêves (2006)/La.Science.Des.Reves.FRENCH.DVDRip.XviD-MP-AceBot.avi +: title: La Science des Rêves + year: 2006 + source: DVD + other: Rip + video_codec: Xvid + video_profile: Main + release_group: AceBot + language: French + +? The_Italian_Job.mkv +: title: The Italian Job + +? The.Rum.Diary.2011.1080p.BluRay.DTS.x264.D-Z0N3.mkv +: title: The Rum Diary + year: 2011 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + audio_codec: DTS + release_group: D-Z0N3 + +? Life.Of.Pi.2012.1080p.BluRay.DTS.x264.D-Z0N3.mkv +: title: Life Of Pi + year: 2012 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + audio_codec: DTS + release_group: D-Z0N3 + +? The.Kings.Speech.2010.1080p.BluRay.DTS.x264.D Z0N3.mkv +: title: The Kings Speech + year: 2010 + screen_size: 1080p + source: Blu-ray + audio_codec: DTS + video_codec: H.264 + release_group: D Z0N3 + +? Street.Kings.2008.BluRay.1080p.DTS.x264.dxva EuReKA.mkv +: title: Street Kings + year: 2008 + source: Blu-ray + screen_size: 1080p + audio_codec: DTS + video_codec: H.264 + video_api: DXVA + release_group: EuReKA + +? 2001.A.Space.Odyssey.1968.HDDVD.1080p.DTS.x264.dxva EuReKA.mkv +: title: 2001 A Space Odyssey + year: 1968 + source: HD-DVD + screen_size: 1080p + audio_codec: DTS + video_codec: H.264 + video_api: DXVA + release_group: EuReKA + +? 2012.2009.720p.BluRay.x264.DTS WiKi.mkv +: title: "2012" + year: 2009 + screen_size: 720p + source: Blu-ray + video_codec: H.264 + audio_codec: DTS + release_group: WiKi + +? /share/Download/movie/Dead Man Down (2013) BRRiP XViD DD5_1 Custom NLSubs =-_lt Q_o_Q gt-=_/XD607ebb-BRc59935-5155473f-1c5f49/XD607ebb-BRc59935-5155473f-1c5f49.avi +: title: Dead Man Down + year: 2013 + source: Blu-ray + other: [Reencoded, Rip] + video_codec: Xvid + audio_channels: "5.1" + audio_codec: Dolby Digital + uuid: XD607ebb-BRc59935-5155473f-1c5f49 + +? Pacific.Rim.3D.2013.COMPLETE.BLURAY-PCH.avi +: title: Pacific Rim + year: 2013 + source: Blu-ray + other: + - Complete + - 3D + release_group: PCH + +? Immersion.French.2011.STV.READNFO.QC.FRENCH.ENGLISH.NTSC.DVDR.nfo +: title: Immersion French + year: 2011 + language: + - French + - English + source: DVD + other: [Straight to Video, Read NFO, NTSC] + +? Immersion.French.2011.STV.READNFO.QC.FRENCH.NTSC.DVDR.nfo +: title: Immersion French + year: 2011 + language: French + source: DVD + other: [Straight to Video, Read NFO, NTSC] + +? Immersion.French.2011.STV.READNFO.QC.NTSC.DVDR.nfo +: title: Immersion + language: French + year: 2011 + source: DVD + other: [Straight to Video, Read NFO, NTSC] + +? French.Immersion.2011.STV.READNFO.QC.ENGLISH.NTSC.DVDR.nfo +: title: French Immersion + year: 2011 + language: ENGLISH + source: DVD + other: [Straight to Video, Read NFO, NTSC] + +? Howl's_Moving_Castle_(2004)_[720p,HDTV,x264,DTS]-FlexGet.avi +: video_codec: H.264 + source: HDTV + title: Howl's Moving Castle + screen_size: 720p + year: 2004 + audio_codec: DTS + release_group: FlexGet + +? Pirates de langkasuka.2008.FRENCH.1920X1080.h264.AVC.AsiaRa.mkv +: screen_size: 1080p + year: 2008 + language: French + video_codec: H.264 + title: Pirates de langkasuka + release_group: AsiaRa + +? Masala (2013) Telugu Movie HD DVDScr XviD - Exclusive.avi +: year: 2013 + video_codec: Xvid + title: Masala + source: HD-DVD + other: Screener + release_group: Exclusive + +? Django Unchained 2012 DVDSCR X264 AAC-P2P.nfo +: year: 2012 + other: Screener + video_codec: H.264 + title: Django Unchained + audio_codec: AAC + source: DVD + release_group: P2P + +? Ejecutiva.En.Apuros(2009).BLURAY.SCR.Xvid.Spanish.LanzamientosD.nfo +: year: 2009 + other: Screener + source: Blu-ray + video_codec: Xvid + language: Spanish + title: Ejecutiva En Apuros + +? Die.Schluempfe.2.German.DL.1080p.BluRay.x264-EXQUiSiTE.mkv +: title: Die Schluempfe 2 + source: Blu-ray + language: + - Multiple languages + - German + video_codec: H.264 + release_group: EXQUiSiTE + screen_size: 1080p + +? Rocky 1976 French SubForced BRRip x264 AC3-FUNKY.mkv +: title: Rocky + year: 1976 + subtitle_language: French + source: Blu-ray + other: [Reencoded, Rip] + video_codec: H.264 + audio_codec: Dolby Digital + release_group: FUNKY + +? REDLINE (BD 1080p H264 10bit FLAC) [3xR].mkv +: title: REDLINE + source: Blu-ray + video_codec: H.264 + color_depth: 10-bit + audio_codec: FLAC + screen_size: 1080p + +? The.Lizzie.McGuire.Movie.(2003).HR.DVDRiP.avi +: title: The Lizzie McGuire Movie + year: 2003 + source: DVD + other: [High Resolution, Rip] + +? Hua.Mulan.BRRIP.MP4.x264.720p-HR.avi +: title: Hua Mulan + video_codec: H.264 + source: Blu-ray + screen_size: 720p + other: [Reencoded, Rip] + release_group: HR + +? Dr.Seuss.The.Lorax.2012.DVDRip.LiNE.XviD.AC3.HQ.Hive-CM8.mp4 +: video_codec: Xvid + title: Dr Seuss The Lorax + source: DVD + other: [Rip, Line Audio] + year: 2012 + audio_codec: Dolby Digital + audio_profile: High Quality + release_group: Hive-CM8 + +? "Star Wars: Episode IV - A New Hope (2004) Special Edition.MKV" +: title: "Star Wars: Episode IV" + alternative_title: A New Hope + year: 2004 + edition: Special + +? Dr.LiNE.The.Lorax.2012.DVDRip.LiNE.XviD.AC3.HQ.Hive-CM8.mp4 +: video_codec: Xvid + title: Dr LiNE The Lorax + source: DVD + other: [Rip, Line Audio] + year: 2012 + audio_codec: Dolby Digital + audio_profile: High Quality + release_group: Hive-CM8 + +? Dr.LiNE.The.Lorax.2012.DVDRip.XviD.AC3.HQ.Hive-CM8.mp4 +: video_codec: Xvid + title: Dr LiNE The Lorax + source: DVD + other: Rip + year: 2012 + audio_codec: Dolby Digital + audio_profile: High Quality + release_group: Hive-CM8 + +? Perfect [email protected] +: release_group: h@mster + title: Perfect Child + video_codec: Xvid + language: French + source: TV + other: Rip + year: 2007 + +? entre.ciel.et.terre.(1994).dvdrip.h264.aac-psypeon.avi +: audio_codec: AAC + source: DVD + other: Rip + release_group: psypeon + title: entre ciel et terre + video_codec: H.264 + year: 1994 + +? Yves.Saint.Laurent.2013.FRENCH.DVDSCR.MD.XviD-ViVARiUM.avi +: source: DVD + language: French + other: [Screener, Mic Dubbed] + release_group: ViVARiUM + title: Yves Saint Laurent + video_codec: Xvid + year: 2013 + +? Echec et Mort - Hard to Kill - Steven Seagal Multi 1080p BluRay x264 CCATS.avi +: source: Blu-ray + language: Multiple languages + release_group: CCATS + screen_size: 1080p + title: Echec et Mort + alternative_title: + - Hard to Kill + - Steven Seagal + video_codec: H.264 + +? Paparazzi - Timsit/Lindon (MKV 1080p tvripHD) +: options: -n + title: Paparazzi + alternative_title: + - Timsit + - Lindon + screen_size: 1080p + container: mkv + source: HDTV + other: Rip + +? some.movie.720p.bluray.x264-mind +: title: some movie + screen_size: 720p + video_codec: H.264 + release_group: mind + source: Blu-ray + +? Dr LiNE The Lorax 720p h264 BluRay +: title: Dr LiNE The Lorax + screen_size: 720p + video_codec: H.264 + source: Blu-ray + +#TODO: Camelcase implementation +#? BeatdownFrenchDVDRip.mkv +#: options: -c +# title: Beatdown +# language: French +# source: DVD + +#? YvesSaintLaurent2013FrenchDVDScrXvid.avi +#: options: -c +# source: DVD +# language: French +# other: Screener +# title: Yves saint laurent +# video_codec: Xvid +# year: 2013 + + +? Elle.s.en.va.720p.mkv +: screen_size: 720p + title: Elle s en va + +? FooBar.7.PDTV-FlexGet +: source: Digital TV + release_group: FlexGet + title: FooBar 7 + +? h265 - HEVC Riddick Unrated Director Cut French 1080p DTS.mkv +: audio_codec: DTS + edition: [Unrated, Director's Cut] + language: fr + screen_size: 1080p + title: Riddick + video_codec: H.265 + +? "[h265 - HEVC] Riddick Unrated Director Cut French [1080p DTS].mkv" +: audio_codec: DTS + edition: [Unrated, Director's Cut] + language: fr + screen_size: 1080p + title: Riddick + video_codec: H.265 + +? Barbecue-2014-French-mHD-1080p +: language: fr + other: Micro HD + screen_size: 1080p + title: Barbecue + year: 2014 + +? Underworld Quadrilogie VO+VFF+VFQ 1080p HDlight.x264~Tonyk~Monde Infernal +: language: fr + other: [Original Video, Micro HD] + screen_size: 1080p + title: Underworld Quadrilogie + video_codec: H.264 + +? A Bout Portant (The Killers).PAL.Multi.DVD-R-KZ +: source: DVD + language: mul + release_group: KZ + title: A Bout Portant + +? "Mise à Sac (Alain Cavalier, 1967) [Vhs.Rip.Vff]" +: source: VHS + language: fr + title: "Mise à Sac" + year: 1967 + +? A Bout Portant (The Killers).PAL.Multi.DVD-R-KZ +: source: DVD + other: PAL + language: mul + release_group: KZ + title: A Bout Portant + +? Youth.In.Revolt.(Be.Bad).2009.MULTI.1080p.LAME3*92-MEDIOZZ +: audio_codec: MP3 + language: mul + release_group: MEDIOZZ + screen_size: 1080p + title: Youth In Revolt + year: 2009 + +? La Defense Lincoln (The Lincoln Lawyer) 2011 [DVDRIP][Vostfr] +: source: DVD + other: Rip + subtitle_language: fr + title: La Defense Lincoln + year: 2011 + +? '[h265 - HEVC] Fight Club French 1080p DTS.' +: audio_codec: DTS + language: fr + screen_size: 1080p + title: Fight Club + video_codec: H.265 + +? Love Gourou (Mike Myers) - FR +: language: fr + title: Love Gourou + +? '[h265 - hevc] transformers 2 1080p french ac3 6ch.' +: audio_channels: '5.1' + audio_codec: Dolby Digital + language: fr + screen_size: 1080p + title: transformers 2 + video_codec: H.265 + +? 1.Angry.Man.1957.mkv +: title: 1 Angry Man + year: 1957 + +? 12.Angry.Men.1957.mkv +: title: 12 Angry Men + year: 1957 + +? 123.Angry.Men.1957.mkv +: title: 123 Angry Men + year: 1957 + +? "Looney Tunes 1444x866 Porky's Last Stand.mkv" +: screen_size: 1444x866 + title: Looney Tunes + +? Das.Appartement.German.AC3D.DL.720p.BluRay.x264-TVP +: audio_codec: Dolby Digital + source: Blu-ray + language: mul + release_group: TVP + screen_size: 720p + title: Das Appartement German + type: movie + video_codec: H.264 + +? Das.Appartement.GERMAN.AC3D.DL.720p.BluRay.x264-TVP +: audio_codec: Dolby Digital + source: Blu-ray + language: + - de + - mul + release_group: TVP + screen_size: 720p + title: Das Appartement + video_codec: H.264 + +? Hyena.Road.2015.German.1080p.DL.DTSHD.Bluray.x264-pmHD +: audio_codec: DTS-HD + source: Blu-ray + language: + - de + - mul + release_group: pmHD + screen_size: 1080p + title: Hyena Road + type: movie + video_codec: H.264 + year: 2015 + +? Hyena.Road.2015.German.1080p.DL.DTSHD.Bluray.x264-pmHD +: audio_codec: DTS-HD + source: Blu-ray + language: + - de + - mul + release_group: pmHD + screen_size: 1080p + title: Hyena Road + type: movie + video_codec: H.264 + year: 2015 + +? Name.BDMux.720p +: title: Name + source: Blu-ray + other: Mux + screen_size: 720p + type: movie + +? Name.BRMux.720p +: title: Name + source: Blu-ray + other: [Reencoded, Mux] + screen_size: 720p + type: movie + +? Name.BDRipMux.720p +: title: Name + source: Blu-ray + other: [Rip, Mux] + screen_size: 720p + type: movie + +? Name.BRRipMux.720p +: title: Name + source: Blu-ray + other: [Reencoded, Rip, Mux] + screen_size: 720p + type: movie + +? Secondary Education (2013).mkv +: options: -T Second + title: Secondary Education + year: 2013 + type: movie + +? Mad Max Beyond Thunderdome () +: title: Mad Max Beyond Thunderdome + type: movie + +? Hacksaw Ridge 2016 Multi 2160p UHD BluRay Hevc10 HDR10 DTSHD & ATMOS 7.1 -DDR.mkv +: title: Hacksaw Ridge + year: 2016 + language: mul + screen_size: 2160p + source: Ultra HD Blu-ray + video_codec: H.265 + color_depth: 10-bit + audio_codec: [DTS-HD, Dolby Atmos] + audio_channels: '7.1' + release_group: DDR + container: mkv + type: movie + +? Special.Correspondents.2016.iTA.ENG.4K.2160p.NetflixUHD.TeamPremium.mp4 +: title: Special Correspondents + year: 2016 + language: [it, en] + screen_size: 2160p + streaming_service: Netflix + other: Ultra HD + release_group: TeamPremium + container: mp4 + type: movie + +? -Special.Correspondents.2016.iTA.ENG.4K.2160p.NetflixUHD.TeamPremium.mp4 +: alternative_title: 4K + +? -Special.Correspondents.2016.iTA.ENG.4K.2160p.NetflixUHD.TeamPremium.mp4 +: alternative_title: 2160p + +? Suicide Squad EXTENDED (2016) 2160p 4K UltraHD Blu-Ray x265 (HEVC 10bit BT709) Dolby Atmos 7.1 -DDR +: title: Suicide Squad + edition: Extended + year: 2016 + screen_size: 2160p + source: Ultra HD Blu-ray + video_codec: H.265 + color_depth: 10-bit + audio_codec: Dolby Atmos + audio_channels: '7.1' + release_group: DDR + type: movie + +? Queen - A Kind of Magic (Alternative Extended Version) 2CD 2014 +: title: Queen + alternative_title: A Kind of Magic + edition: [Alternative Cut, Extended] + cd_count: 2 + year: 2014 + type: movie + +? Jour.de.Fete.1949.ALTERNATiVE.CUT.1080p.BluRay.x264-SADPANDA[rarbg] +: title: Jour de Fete + year: 1949 + edition: Alternative Cut + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: SADPANDA[rarbg] + +? The.Movie.CONVERT.720p.HDTV.x264-C4TV +: title: The Movie + other: Converted + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: C4TV + type: movie + +? Its.A.Wonderful.Life.1946.Colorized.720p.BRRip.999MB.MkvCage.com +: title: Its A Wonderful Life + year: 1946 + other: [Colorized, Reencoded, Rip] + screen_size: 720p + source: Blu-ray + size: 999MB + website: MkvCage.com + type: movie + +? Alien DC (1979) [1080p] +: title: Alien + edition: Director's Cut + year: 1979 + screen_size: 1080p + type: movie + +? Requiem.For.A.Dream.2000.DC.1080p.BluRay.x264.anoXmous +: title: Requiem For A Dream + year: 2000 + edition: Director's Cut + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: anoXmous + type: movie + +? Before.the.Flood.2016.DOCU.1080p.WEBRip.x264.DD5.1-FGT +: title: Before the Flood + year: 2016 + other: [Documentary, Rip] + screen_size: 1080p + source: Web + video_codec: H.264 + audio_codec: Dolby Digital + audio_channels: '5.1' + release_group: FGT + type: movie + +? Zootopia.2016.HDRip.1.46Gb.Dub.MegaPeer +: title: Zootopia + year: 2016 + other: [HD, Rip] + size: 1.46GB + language: und + release_group: MegaPeer + type: movie + +? Suntan.2016.FESTiVAL.DVDRip.x264-IcHoR +: title: Suntan + year: 2016 + edition: Festival + source: DVD + other: Rip + video_codec: H.264 + release_group: IcHoR + type: movie + +? Hardwired.STV.NFOFiX.FRENCH.DVDRiP.XviD-SURViVAL +: title: Hardwired + other: [Straight to Video, Proper, Rip] + language: french + source: DVD + video_codec: Xvid + release_group: SURViVAL + proper_count: 1 + type: movie + +? Maze.Runner.The.Scorch.Trials.OM.2015.WEB-DLRip.by.Seven +: title: Maze Runner The Scorch Trials + other: [Open Matte, Rip] + year: 2015 + source: Web + release_group: Seven + type: movie + +? Kampen Om Tungtvannet aka The Heavy Water War COMPLETE 720p x265 HEVC-Lund +: title: Kampen Om Tungtvannet aka The Heavy Water War + other: Complete + screen_size: 720p + video_codec: H.265 + release_group: Lund + type: movie + +? All.Fall.Down.x264.PROOFFIX-OUTLAWS +: title: All Fall Down + video_codec: H.264 + other: Proper + release_group: OUTLAWS + proper_count: 1 + type: movie + +? The.Last.Survivors.2014.PROOF.SAMPLE.FiX.BDRip.x264-TOPCAT +: title: The Last Survivors + year: 2014 + other: [Proper, Rip] + source: Blu-ray + video_codec: H.264 + release_group: TOPCAT + type: movie + +? Bad Santa 2 2016 THEATRiCAL FRENCH BDRip XviD-EXTREME +: title: Bad Santa 2 + year: 2016 + edition: Theatrical + language: french + source: Blu-ray + other: Rip + video_codec: Xvid + release_group: EXTREME + type: movie + +? The Lord of the Rings The Fellowship of the Ring THEATRICAL EDITION (2001) [1080p] +: title: The Lord of the Rings The Fellowship of the Ring + edition: Theatrical + year: 2001 + screen_size: 1080p + type: movie + +? World War Z (2013) Theatrical Cut 720p BluRay x264 +: title: World War Z + year: 2013 + edition: Theatrical + screen_size: 720p + source: Blu-ray + video_codec: H.264 + type: movie + +? The Heartbreak Kid (1993) UNCUT 720p WEBRip x264 +: title: The Heartbreak Kid + year: 1993 + edition: Uncut + other: Rip + screen_size: 720p + source: Web + video_codec: H.264 + type: movie + +? Mrs.Doubtfire.1993.720p.OAR.Bluray.DTS.x264-CtrlHD +: title: Mrs Doubtfire + year: 1993 + screen_size: 720p + other: Original Aspect Ratio + source: Blu-ray + audio_codec: DTS + video_codec: H.264 + release_group: CtrlHD + type: movie + +? Aliens.SE.1986.BDRip.1080p +: title: Aliens + edition: Special + year: 1986 + source: Blu-ray + other: Rip + screen_size: 1080p + type: movie + +? 10 Cloverfield Lane.[Blu-Ray 1080p].[MULTI] +: options: --type movie + title: 10 Cloverfield Lane + source: Blu-ray + screen_size: 1080p + language: Multiple languages + type: movie + +? 007.Spectre.[HDTC.MD].[TRUEFRENCH] +: options: --type movie + title: 007 Spectre + source: HD Telecine + language: French + type: movie + +? We.Are.X.2016.LIMITED.BDRip.x264-BiPOLAR +: title: We Are X + year: 2016 + edition: Limited + source: Blu-ray + other: Rip + video_codec: H.264 + release_group: BiPOLAR + type: movie + +? The Rack (VHS) [1956] Paul Newman +: title: The Rack + source: VHS + year: 1956 + type: movie + +? Les.Magiciens.1976.VHSRip.XViD.MKO +: title: Les Magiciens + year: 1976 + source: VHS + other: Rip + video_codec: Xvid + release_group: MKO + type: movie + +? The Boss Baby 2017 720p CAM x264 AC3 TiTAN +: title: The Boss Baby + year: 2017 + screen_size: 720p + source: Camera + video_codec: H.264 + audio_codec: Dolby Digital + release_group: TiTAN + type: movie + +? The.Boss.Baby.2017.HDCAM.XviD-MrGrey +: title: The Boss Baby + year: 2017 + source: HD Camera + video_codec: Xvid + release_group: MrGrey + type: movie + +? The Martian 2015 Multi 2160p 4K UHD Bluray HEVC10 SDR DTSHD 7.1 -Zeus +: title: The Martian + year: 2015 + language: mul + screen_size: 2160p + source: Ultra HD Blu-ray + video_codec: H.265 + color_depth: 10-bit + other: Standard Dynamic Range + audio_codec: DTS-HD + audio_channels: '7.1' + release_group: Zeus + type: movie + +? Fantastic Beasts and Where to Find Them 2016 Multi 2160p UHD BluRay HEVC HDR Atmos7.1-DDR +: title: Fantastic Beasts and Where to Find Them + year: 2016 + language: mul + screen_size: 2160p + source: Ultra HD Blu-ray + video_codec: H.265 + other: HDR10 + audio_codec: Dolby Atmos + audio_channels: '7.1' + release_group: DDR + type: movie + +? Life of Pi 2012 2160p 4K BluRay HDR10 HEVC BT2020 DTSHD 7.1 subs -DDR +: title: Life of Pi + year: 2012 + screen_size: 2160p + source: Ultra HD Blu-ray + other: [HDR10, BT.2020] + subtitle_language: und + release_group: DDR + +? Captain.America.Civil.War.HDR.1080p.HEVC.10bit.BT.2020.DTS-HD.MA.7.1-VISIONPLUSHDR +: title: Captain America Civil War + other: [HDR10, BT.2020] + screen_size: 1080p + video_codec: H.265 + color_depth: 10-bit + audio_codec: DTS-HD + audio_profile: Master Audio + audio_channels: '7.1' + release_group: VISIONPLUSHDR + type: movie + +? Deadpool.2016.4K.2160p.UHD.HQ.8bit.BluRay.8CH.x265.HEVC-MZABI.mkv +: title: Deadpool + year: 2016 + screen_size: 2160p + source: Ultra HD Blu-ray + other: High Quality + color_depth: 8-bit + audio_channels: '7.1' + video_codec: H.265 + release_group: MZABI + type: movie + +? Fantastic.Beasts.and.Where.to.Find.Them.2016.2160p.4K.UHD.10bit.HDR.BluRay.7.1.x265.HEVC-MZABI.mkv +: title: Fantastic Beasts and Where to Find Them + year: 2016 + screen_size: 2160p + source: Ultra HD Blu-ray + color_depth: 10-bit + other: HDR10 + audio_channels: '7.1' + video_codec: H.265 + release_group: MZABI + container: mkv + type: movie + +? The.Arrival.4K.HDR.HEVC.10bit.BT2020.DTS.HD-MA-MadVR.HDR10.Dolby.Vision-VISIONPLUSHDR1000 +: title: The Arrival + screen_size: 2160p + other: [HDR10, BT.2020, Dolby Vision] + video_codec: H.265 + color_depth: 10-bit + audio_codec: DTS-HD + audio_profile: Master Audio + release_group: VISIONPLUSHDR1000 + type: movie + +? How To Steal A Dog.2014.BluRay.1080p.12bit.HEVC.OPUS 5.1-Hn1Dr2.mkv +: title: How To Steal A Dog + year: 2014 + source: Blu-ray + screen_size: 1080p + color_depth: 12-bit + video_codec: H.265 + audio_codec: Opus + audio_channels: '5.1' + release_group: Hn1Dr2 + container: mkv + type: movie + +? Interstelar.2014.IMAX.RUS.BDRip.x264.-HELLYWOOD.mkv +: title: Interstelar + year: 2014 + edition: IMAX + language: ru + source: Blu-ray + other: Rip + video_codec: H.264 + release_group: HELLYWOOD + container: mkv + type: movie + +? The.Dark.Knight.IMAX.EDITION.HQ.BluRay.1080p.x264.AC3.Hindi.Eng.ETRG +: title: The Dark Knight + edition: IMAX + other: High Quality + source: Blu-ray + screen_size: 1080p + video_codec: H.264 + audio_codec: Dolby Digital + language: [hindi, english] + release_group: ETRG + type: movie + +? The.Martian.2015.4K.UHD.UPSCALED-ETRG +: title: The Martian + year: 2015 + screen_size: 2160p + other: [Ultra HD, Upscaled] + release_group: ETRG + type: movie + +? Delibal 2015 720p Upscale DVDRip x264 DD5.1 AC3 +: title: Delibal + year: 2015 + screen_size: 720p + other: [Upscaled, Rip] + source: DVD + video_codec: H.264 + audio_codec: Dolby Digital + audio_channels: '5.1' + type: movie + +? Casablanca [Ultimate Collector's Edition].1942.BRRip.XviD-VLiS +: title: Casablanca + edition: [Ultimate, Collector] + year: 1942 + source: Blu-ray + other: [Reencoded, Rip] + video_codec: Xvid + release_group: VLiS + type: movie + +? Batman V Superman Dawn of Justice 2016 Extended Cut Ultimate Edition HDRip x264 AC3-DaDDy +: title: Batman V Superman Dawn of Justice + year: 2016 + edition: [Extended, Ultimate] + other: [HD, Rip] + video_codec: H.264 + audio_codec: Dolby Digital + release_group: DaDDy + type: movie + +? Stargate SG1 Ultimate Fan Collection +: title: Stargate SG1 + edition: [Ultimate, Fan] + +? The.Jungle.Book.2016.MULTi.1080p.BluRay.x264.DTS-HD.MA.7.1.DTS-HD.HRA.5.1-LeRalou +: title: The Jungle Book + year: 2016 + language: mul + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + audio_codec: DTS-HD + audio_profile: [Master Audio, High Resolution Audio] + audio_channels: ['7.1', '5.1'] + release_group: LeRalou + type: movie + +? Terminus.2015.BluRay.1080p.x264.DTS-HD.HRA.5.1-LTT +: title: Terminus + year: 2015 + source: Blu-ray + screen_size: 1080p + video_codec: H.264 + audio_codec: DTS-HD + audio_profile: High Resolution Audio + audio_channels: '5.1' + release_group: LTT + type: movie + +? Ghost.in.the.Shell.1995.1080p.Bluray.DTSES.x264-SHiTSoNy +: title: Ghost in the Shell + year: 1995 + screen_size: 1080p + source: Blu-ray + audio_codec: DTS + audio_profile: Extended Surround + +? The.Boss.Baby.2017.BluRay.1080p.DTS-ES.x264-PRoDJi +: title: The Boss Baby + year: 2017 + source: Blu-ray + screen_size: 1080p + audio_codec: DTS + audio_profile: Extended Surround + video_codec: H.264 + release_group: PRoDJi + type: movie + +? Title.2000.720p.BluRay.DDEX.x264-HDClub.mkv +: title: Title + year: 2000 + screen_size: 720p + source: Blu-ray + audio_codec: Dolby Digital + audio_profile: EX + video_codec: H.264 + release_group: HDClub + container: mkv + type: movie + +? Jack Reacher Never Go Back 2016 720p Bluray DD-EX x264-BluPanther +: title: Jack Reacher Never Go Back + year: 2016 + screen_size: 720p + source: Blu-ray + audio_codec: Dolby Digital + audio_profile: EX + video_codec: H.264 + release_group: BluPanther + type: movie + +? How To Steal A Dog.2014.BluRay.1080p.12bit.HEVC.OPUS 5.1-Hn1Dr2.mkv +: title: How To Steal A Dog + year: 2014 + source: Blu-ray + screen_size: 1080p + color_depth: 12-bit + video_codec: H.265 + audio_codec: Opus + audio_channels: '5.1' + release_group: Hn1Dr2 + container: mkv + type: movie + +? How.To.Be.Single.2016.1080p.BluRay.x264-BLOW/blow-how.to.be.single.2016.1080p.bluray.x264.mkv +: title: How To Be Single + year: 2016 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: BLOW + container: mkv + type: movie + +? After.the.Storm.2016.720p.YIFY +: title: After the Storm + year: 2016 + screen_size: 720p + release_group: YIFY + type: movie + +? Battle Royale 2000 DC (1080p Bluray x265 HEVC 10bit AAC 7.1 Japanese Tigole) +: title: Battle Royale + year: 2000 + edition: Director's Cut + screen_size: 1080p + source: Blu-ray + video_codec: H.265 + color_depth: 10-bit + audio_codec: AAC + audio_channels: '7.1' + language: jp + release_group: Tigole + +? Congo.The.Grand.Inga.Project.2013.1080p.BluRay.x264-OBiTS +: title: Congo The Grand Inga Project + year: 2013 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: OBiTS + type: movie + +? Congo.The.Grand.Inga.Project.2013.BRRip.XviD.MP3-RARBG +: title: Congo The Grand Inga Project + year: 2013 + source: Blu-ray + other: [Reencoded, Rip] + video_codec: Xvid + audio_codec: MP3 + release_group: RARBG + type: movie + +? Congo.The.Grand.Inga.Project.2013.720p.BluRay.H264.AAC-RARBG +: title: Congo The Grand Inga Project + year: 2013 + screen_size: 720p + source: Blu-ray + video_codec: H.264 + audio_codec: AAC + release_group: RARBG + type: movie + +? Mit.dem.Bauch.durch.die.Wand.SWiSSGERMAN.DOKU.DVDRiP.x264-DEFLOW +: title: Mit dem Bauch durch die Wand + language: de-CH + other: [Documentary, Rip] + source: DVD + video_codec: H.264 + release_group: DEFLOW + type: movie + +? InDefinitely.Maybe.2008.1080p.EUR.BluRay.VC-1.DTS-HD.MA.5.1-FGT +: title: InDefinitely Maybe + year: 2008 + screen_size: 1080p + source: Blu-ray + video_codec: VC-1 + audio_codec: DTS-HD + audio_profile: Master Audio + audio_channels: '5.1' + release_group: FGT + type: movie + +? Bjyukujyo Kyoushi Kan XXX 720P WEBRIP MP4-GUSH +: title: Bjyukujyo Kyoushi Kan + other: [XXX, Rip] + screen_size: 720p + source: Web + container: mp4 + release_group: GUSH + type: movie + +? The.Man.With.The.Golden.Arm.1955.1080p.BluRay.x264.DTS-FGT +: title: The Man With The Golden Arm + year: 1955 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + audio_codec: DTS + release_group: FGT + type: movie + +? blow-how.to.be.single.2016.1080p.bluray.x264.mkv +: release_group: blow + title: how to be single + year: 2016 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + container: mkv + type: movie + +? ulshd-the.right.stuff.1983.multi.1080p.bluray.x264.mkv +: release_group: ulshd + title: the right stuff + year: 1983 + language: mul + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + container: mkv + type: movie + +? FROZEN [2010] LiMiTED DVDRip H262 AAC[ ENG SUBS]-MANTESH +: title: FROZEN + year: 2010 + edition: Limited + source: DVD + other: Rip + video_codec: MPEG-2 + audio_codec: AAC + subtitle_language: english + release_group: MANTESH + type: movie + +? Family.Katta.2016.1080p.WEB-DL.H263.DD5.1.ESub-DDR +: title: Family Katta + year: 2016 + screen_size: 1080p + source: Web + video_codec: H.263 + audio_codec: Dolby Digital + audio_channels: '5.1' + subtitle_language: und + release_group: DDR + type: movie + +? Bad Boys 2 1080i.mpg2.rus.eng.ts +: title: Bad Boys 2 + screen_size: 1080i + video_codec: MPEG-2 + language: [russian, english] + container: ts + type: movie + +? Alien.Director.Cut.Ita.Eng.VP9.Opus.AlphaBot.webm +: title: Alien + edition: Director's Cut + language: [english, italian] + video_codec: VP9 + audio_codec: Opus + release_group: AlphaBot + container: webm + type: movie + +? The.Stranger.1946.US.(Kino.Classics).Bluray.1080p.LPCM.DD-2.0.x264-Grym@BTNET +: title: The Stranger + year: 1946 + country: US + source: Blu-ray + screen_size: 1080p + audio_codec: [LPCM, Dolby Digital] + audio_channels: '2.0' + video_codec: H.264 + release_group: Grym@BTNET + type: movie + +? X-Men.Apocalypse.2016.complete.hdts.pcm.TrueFrench-Scarface45.avi +: title: X-Men Apocalypse + year: 2016 + other: Complete + source: HD Telesync + audio_codec: PCM + language: french + release_group: Scarface45 + container: avi + type: movie + +? Tears.of.Steel.2012.2160p.DMRip.Eng.HDCLUB.mkv +: title: Tears of Steel + year: 2012 + screen_size: 2160p + source: Digital Master + other: Rip + language: english + release_group: HDCLUB + container: mkv + type: movie + +? "/Movies/Open Season 2 (2008)/Open Season 2 (2008) - Bluray-1080p.x264.DTS.mkv" +: options: --type movie + title: Open Season 2 + year: 2008 + source: Blu-ray + screen_size: 1080p + video_codec: H.264 + audio_codec: DTS + container: mkv + type: movie + +? Re-Animator.1985.INTEGRAL VERSION LIMITED EDITION.1080p.BluRay.REMUX.AVC.DTS-HD MA 5.1-LAZY +: title: Re-Animator + year: 1985 + edition: Limited + screen_size: 1080p + source: Blu-ray + other: Remux + video_codec: H.264 + audio_codec: DTS-HD + audio_profile: Master Audio + audio_channels: '5.1' + release_group: LAZY + type: movie + +? Test (2013) [WEBDL-1080p] [x264 AC3] [ENG+RU+PT] [NTb].mkv +: title: Test + year: 2013 + source: Web + screen_size: 1080p + video_codec: H.264 + audio_codec: Dolby Digital + language: [en, ru, pt] + release_group: NTb + container: mkv + type: movie + +? "[nextorrent.org] Bienvenue.Au.Gondwana.2016.FRENCH.DVDRiP.XViD-AViTECH.avi" +: website: nextorrent.org + title: Bienvenue Au Gondwana + year: 2016 + language: french + source: DVD + other: Rip + video_codec: Xvid + release_group: AViTECH + container: avi + type: movie + +? Star Trek First Contact (1996) Blu-Ray 1080p24 H.264 TrueHD 5.1 CtrlHD +: title: Star Trek First Contact + year: 1996 + source: Blu-ray + screen_size: 1080p + frame_rate: 24fps + video_codec: H.264 + audio_codec: Dolby TrueHD + audio_channels: '5.1' + release_group: CtrlHD + type: movie + +? The.Hobbit.The.Desolation.of.Smaug.Extended.HFR.48fps.ITA.ENG.AC3.BDRip.1080p.x264_ZMachine.mkv +: title: The Hobbit The Desolation of Smaug + edition: Extended + other: [High Frame Rate, Rip] + frame_rate: 48fps + language: [it, en] + audio_codec: Dolby Digital + source: Blu-ray + screen_size: 1080p + video_codec: H.264 + release_group: ZMachine + container: mkv + type: movie + +? Test (2013) [WEBDL-1080p] [x264 AC3] [ENG+PT+DE] [STANDARD] +: title: Test + year: 2013 + source: Web + screen_size: 1080p + video_codec: H.264 + audio_codec: Dolby Digital + language: [en, pt, de] + release_group: STANDARD + type: movie + +? Test (2013) [WEBDL-1080p] [x264 AC3] [ENG+DE+IT] [STANDARD] +: title: Test + year: 2013 + source: Web + screen_size: 1080p + video_codec: H.264 + audio_codec: Dolby Digital + language: [en, de, it] + release_group: STANDARD + type: movie diff --git a/libs/guessit/test/rules/__init__.py b/libs/guessit/test/rules/__init__.py new file mode 100644 index 000000000..e5be370e4 --- /dev/null +++ b/libs/guessit/test/rules/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name diff --git a/libs/guessit/test/rules/audio_codec.yml b/libs/guessit/test/rules/audio_codec.yml new file mode 100644 index 000000000..cbb6fc8bc --- /dev/null +++ b/libs/guessit/test/rules/audio_codec.yml @@ -0,0 +1,131 @@ +# Multiple input strings having same expected results can be chained. +# Use $ marker to check inputs that should not match results. + + +? +MP3 +? +lame +? +lame3.12 +? +lame3.100 +: audio_codec: MP3 + +? +DolbyDigital +? +DD +? +Dolby Digital +? +AC3 +: audio_codec: Dolby Digital + +? +DDP +? +DD+ +? +EAC3 +: audio_codec: Dolby Digital Plus + +? +DolbyAtmos +? +Dolby Atmos +? +Atmos +? -Atmosphere +: audio_codec: Dolby Atmos + +? +AAC +: audio_codec: AAC + +? +Flac +: audio_codec: FLAC + +? +DTS +: audio_codec: DTS + +? +True-HD +? +trueHD +: audio_codec: Dolby TrueHD + +? +True-HD51 +? +trueHD51 +: audio_codec: Dolby TrueHD + audio_channels: '5.1' + +? +DTSHD +? +DTS HD +? +DTS-HD +: audio_codec: DTS-HD + +? +DTS-HDma +? +DTSMA +: audio_codec: DTS-HD + audio_profile: Master Audio + +? +AC3-hq +: audio_codec: Dolby Digital + audio_profile: High Quality + +? +AAC-HE +: audio_codec: AAC + audio_profile: High Efficiency + +? +AAC-LC +: audio_codec: AAC + audio_profile: Low Complexity + +? +AAC2.0 +? +AAC20 +: audio_codec: AAC + audio_channels: '2.0' + +? +7.1 +? +7ch +? +8ch +: audio_channels: '7.1' + +? +5.1 +? +5ch +? +6ch +: audio_channels: '5.1' + +? +2ch +? +2.0 +? +stereo +: audio_channels: '2.0' + +? +1ch +? +mono +: audio_channels: '1.0' + +? DD5.1 +? DD51 +: audio_codec: Dolby Digital + audio_channels: '5.1' + +? -51 +: audio_channels: '5.1' + +? DTS-HD.HRA +? DTSHD.HRA +? DTS-HD.HR +? DTSHD.HR +? -HRA +? -HR +: audio_codec: DTS-HD + audio_profile: High Resolution Audio + +? DTSES +? DTS-ES +? -ES +: audio_codec: DTS + audio_profile: Extended Surround + +? DD-EX +? DDEX +? -EX +: audio_codec: Dolby Digital + audio_profile: EX + +? OPUS +: audio_codec: Opus + +? Vorbis +: audio_codec: Vorbis + +? PCM +: audio_codec: PCM + +? LPCM +: audio_codec: LPCM diff --git a/libs/guessit/test/rules/bonus.yml b/libs/guessit/test/rules/bonus.yml new file mode 100644 index 000000000..6ef6f5b25 --- /dev/null +++ b/libs/guessit/test/rules/bonus.yml @@ -0,0 +1,9 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Movie Title-x01-Other Title.mkv +? Movie Title-x01-Other Title +? directory/Movie Title-x01-Other Title/file.mkv +: title: Movie Title + bonus_title: Other Title + bonus: 1 + diff --git a/libs/guessit/test/rules/cds.yml b/libs/guessit/test/rules/cds.yml new file mode 100644 index 000000000..d76186c6b --- /dev/null +++ b/libs/guessit/test/rules/cds.yml @@ -0,0 +1,10 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? cd 1of3 +: cd: 1 + cd_count: 3 + +? Some.Title-DVDRIP-x264-CDP +: cd: !!null + release_group: CDP + video_codec: H.264 diff --git a/libs/guessit/test/rules/country.yml b/libs/guessit/test/rules/country.yml new file mode 100644 index 000000000..7e9693980 --- /dev/null +++ b/libs/guessit/test/rules/country.yml @@ -0,0 +1,13 @@ +# Multiple input strings having same expected results can be chained. +# Use $ marker to check inputs that should not match results. +? Us.this.is.title +? this.is.title.US +: country: US + title: this is title + +? This.is.us.title +: title: This is us title + +? This.Is.Us +: options: --no-embedded-config + title: This Is Us diff --git a/libs/guessit/test/rules/date.yml b/libs/guessit/test/rules/date.yml new file mode 100644 index 000000000..d7379f03c --- /dev/null +++ b/libs/guessit/test/rules/date.yml @@ -0,0 +1,50 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +09.03.08 +? +09.03.2008 +? +2008.03.09 +: date: 2008-03-09 + +? +31.01.15 +? +31.01.2015 +? +15.01.31 +? +2015.01.31 +: date: 2015-01-31 + +? +01.02.03 +: date: 2003-02-01 + +? +01.02.03 +: options: --date-year-first + date: 2001-02-03 + +? +01.02.03 +: options: --date-day-first + date: 2003-02-01 + +? 1919 +? 2030 +: !!map {} + +? 2029 +: year: 2029 + +? (1920) +: year: 1920 + +? 2012 +: year: 2012 + +? 2011 2013 (2012) (2015) # first marked year is guessed. +: title: "2011 2013" + year: 2012 + +? 2012 2009 S01E02 2015 # If no year is marked, the second one is guessed. +: title: "2012" + year: 2009 + episode_title: "2015" + +? Something 2 mar 2013) +: title: Something + date: 2013-03-02 + type: episode diff --git a/libs/guessit/test/rules/edition.yml b/libs/guessit/test/rules/edition.yml new file mode 100644 index 000000000..4b7fd9866 --- /dev/null +++ b/libs/guessit/test/rules/edition.yml @@ -0,0 +1,63 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Director's cut +? Edition Director's cut +: edition: Director's Cut + +? Collector +? Collector Edition +? Edition Collector +: edition: Collector + +? Special Edition +? Edition Special +? -Special +: edition: Special + +? Criterion Edition +? Edition Criterion +? CC +? -Criterion +: edition: Criterion + +? Deluxe +? Deluxe Edition +? Edition Deluxe +: edition: Deluxe + +? Super Movie Alternate XViD +? Super Movie Alternative XViD +? Super Movie Alternate Cut XViD +? Super Movie Alternative Cut XViD +: edition: Alternative Cut + +? ddc +: edition: Director's Definitive Cut + +? IMAX +? IMAX Edition +: edition: IMAX + +? ultimate edition +? -ultimate +: edition: Ultimate + +? ultimate collector edition +? ultimate collector's edition +? ultimate collectors edition +? -collectors edition +? -ultimate edition +: edition: [Ultimate, Collector] + +? ultimate collectors edition dc +: edition: [Ultimate, Collector, Director's Cut] + +? fan edit +? fan edition +? fan collection +: edition: Fan + +? ultimate fan edit +? ultimate fan edition +? ultimate fan collection +: edition: [Ultimate, Fan] diff --git a/libs/guessit/test/rules/episodes.yml b/libs/guessit/test/rules/episodes.yml new file mode 100644 index 000000000..98325fa1d --- /dev/null +++ b/libs/guessit/test/rules/episodes.yml @@ -0,0 +1,279 @@ +# Multiple input strings having same expected results can be chained. +# Use $ marker to check inputs that should not match results. +? +2x5 +? +2X5 +? +02x05 +? +2X05 +? +02x5 +? S02E05 +? s02e05 +? s02e5 +? s2e05 +? s02ep05 +? s2EP5 +? -s03e05 +? -s02e06 +? -3x05 +? -2x06 +: season: 2 + episode: 5 + +? "+0102" +? "+102" +: season: 1 + episode: 2 + +? "0102 S03E04" +? "S03E04 102" +: season: 3 + episode: 4 + +? +serie Saison 2 other +? +serie Season 2 other +? +serie Saisons 2 other +? +serie Seasons 2 other +? +serie Serie 2 other +? +serie Series 2 other +? +serie Season Two other +? +serie Season II other +: season: 2 + +? Some Series.S02E01.Episode.title.mkv +? Some Series/Season 02/E01-Episode title.mkv +? Some Series/Season 02/Some Series-E01-Episode title.mkv +? Some Dummy Directory/Season 02/Some Series-E01-Episode title.mkv +? -Some Dummy Directory/Season 02/E01-Episode title.mkv +? Some Series/Unsafe Season 02/Some Series-E01-Episode title.mkv +? -Some Series/Unsafe Season 02/E01-Episode title.mkv +? Some Series/Season 02/E01-Episode title.mkv +? Some Series/ Season 02/E01-Episode title.mkv +? Some Dummy Directory/Some Series S02/E01-Episode title.mkv +? Some Dummy Directory/S02 Some Series/E01-Episode title.mkv +: title: Some Series + episode_title: Episode title + season: 2 + episode: 1 + +? Some Series.S02E01.mkv +? Some Series/Season 02/E01.mkv +? Some Series/Season 02/Some Series-E01.mkv +? Some Dummy Directory/Season 02/Some Series-E01.mkv +? -Some Dummy Directory/Season 02/E01.mkv +? Some Series/Unsafe Season 02/Some Series-E01.mkv +? -Some Series/Unsafe Season 02/E01.mkv +? Some Series/Season 02/E01.mkv +? Some Series/ Season 02/E01.mkv +? Some Dummy Directory/Some Series S02/E01-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA.mkv +: title: Some Series + season: 2 + episode: 1 + +? Some Series S03E01E02 +: title: Some Series + season: 3 + episode: [1, 2] + +? Some Series S01S02S03 +? Some Series S01-02-03 +? Some Series S01 S02 S03 +? Some Series S01 02 03 +: title: Some Series + season: [1, 2, 3] + +? Some Series E01E02E03 +? Some Series E01-02-03 +? Some Series E01-03 +? Some Series E01 E02 E03 +? Some Series E01 02 03 +: title: Some Series + episode: [1, 2, 3] + +? Some Series E01E02E04 +? Some Series E01 E02 E04 +? Some Series E01 02 04 +: title: Some Series + episode: [1, 2, 4] + +? Some Series E01-02-04 +? Some Series E01-04 +? Some Series E01-04 +: title: Some Series + episode: [1, 2, 3, 4] + +? Some Series E01-02-E04 +: title: Some Series + episode: [1, 2, 3, 4] + +? Episode 3 +? -Episode III +: episode: 3 + +? Episode 3 +? Episode III +: options: -t episode + episode: 3 + +? -A very special movie +: episode_details: Special + +? -A very special episode +: options: -t episode + episode_details: Special + +? A very special episode s06 special +: options: -t episode + title: A very special episode + episode_details: Special + +? 12 Monkeys\Season 01\Episode 05\12 Monkeys - S01E05 - The Night Room.mkv +: container: mkv + title: 12 Monkeys + episode: 5 + season: 1 + +? S03E02.X.1080p +: episode: 2 + screen_size: 1080p + season: 3 + +? Something 1 x 2-FlexGet +: options: -t episode + title: Something + season: 1 + episode: 2 + episode_title: FlexGet + +? Show.Name.-.Season.1.to.3.-.Mp4.1080p +? Show.Name.-.Season.1~3.-.Mp4.1080p +? Show.Name.-.Saison.1.a.3.-.Mp4.1080p +: container: mp4 + screen_size: 1080p + season: + - 1 + - 2 + - 3 + title: Show Name + +? Show.Name.Season.1.3&5.HDTV.XviD-GoodGroup[SomeTrash] +? Show.Name.Season.1.3 and 5.HDTV.XviD-GoodGroup[SomeTrash] +: source: HDTV + release_group: GoodGroup[SomeTrash] + season: + - 1 + - 3 + - 5 + title: Show Name + type: episode + video_codec: Xvid + +? Show.Name.Season.1.2.3-5.HDTV.XviD-GoodGroup[SomeTrash] +? Show.Name.Season.1.2.3~5.HDTV.XviD-GoodGroup[SomeTrash] +? Show.Name.Season.1.2.3 to 5.HDTV.XviD-GoodGroup[SomeTrash] +: source: HDTV + release_group: GoodGroup[SomeTrash] + season: + - 1 + - 2 + - 3 + - 4 + - 5 + title: Show Name + type: episode + video_codec: Xvid + +? The.Get.Down.S01EP01.FRENCH.720p.WEBRIP.XVID-STR +: episode: 1 + source: Web + other: Rip + language: fr + release_group: STR + screen_size: 720p + season: 1 + title: The Get Down + type: episode + video_codec: Xvid + +? My.Name.Is.Earl.S01E01-S01E21.SWE-SUB +: episode: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + - 13 + - 14 + - 15 + - 16 + - 17 + - 18 + - 19 + - 20 + - 21 + season: 1 + subtitle_language: sv + title: My Name Is Earl + type: episode + +? Show.Name.Season.4.Episodes.1-12 +: episode: + - 1 + - 2 + - 3 + - 4 + - 5 + - 6 + - 7 + - 8 + - 9 + - 10 + - 11 + - 12 + season: 4 + title: Show Name + type: episode + +? show name s01.to.s04 +: season: + - 1 + - 2 + - 3 + - 4 + title: show name + type: episode + +? epi +: options: -t episode + title: epi + +? Episode20 +? Episode 20 +: episode: 20 + +? Episode50 +? Episode 50 +: episode: 50 + +? Episode51 +? Episode 51 +: episode: 51 + +? Episode70 +? Episode 70 +: episode: 70 + +? Episode71 +? Episode 71 +: episode: 71 + +? S01D02.3-5-GROUP +: disc: [2, 3, 4, 5] + +? S01D02&4-6&8 +: disc: [2, 4, 5, 6, 8] diff --git a/libs/guessit/test/rules/film.yml b/libs/guessit/test/rules/film.yml new file mode 100644 index 000000000..1f7743318 --- /dev/null +++ b/libs/guessit/test/rules/film.yml @@ -0,0 +1,9 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Film Title-f01-Series Title.mkv +? Film Title-f01-Series Title +? directory/Film Title-f01-Series Title/file.mkv +: title: Series Title + film_title: Film Title + film: 1 + diff --git a/libs/guessit/test/rules/language.yml b/libs/guessit/test/rules/language.yml new file mode 100644 index 000000000..10e5b9c05 --- /dev/null +++ b/libs/guessit/test/rules/language.yml @@ -0,0 +1,47 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +English +? .ENG. +: language: English + +? +French +: language: French + +? +SubFrench +? +SubFr +? +STFr +? ST.FR +: subtitle_language: French + +? +ENG.-.sub.FR +? ENG.-.FR Sub +? +ENG.-.SubFR +? +ENG.-.FRSUB +? +ENG.-.FRSUBS +? +ENG.-.FR-SUBS +: language: English + subtitle_language: French + +? "{Fr-Eng}.St{Fr-Eng}" +? "Le.Prestige[x264.{Fr-Eng}.St{Fr-Eng}.Chaps].mkv" +: language: [French, English] + subtitle_language: [French, English] + +? +ENG.-.sub.SWE +? ENG.-.SWE Sub +? +ENG.-.SubSWE +? +ENG.-.SWESUB +? +ENG.-.sub.SV +? ENG.-.SV Sub +? +ENG.-.SubSV +? +ENG.-.SVSUB +: language: English + subtitle_language: Swedish + +? The English Patient (1996) +: title: The English Patient + -language: english + +? French.Kiss.1995.1080p +: title: French Kiss + -language: french diff --git a/libs/guessit/test/rules/other.yml b/libs/guessit/test/rules/other.yml new file mode 100644 index 000000000..113b6d813 --- /dev/null +++ b/libs/guessit/test/rules/other.yml @@ -0,0 +1,172 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +DVDSCR +? +DVDScreener +? +DVD-SCR +? +DVD Screener +? +DVD AnythingElse Screener +? -DVD AnythingElse SCR +: other: Screener + +? +AudioFix +? +AudioFixed +? +Audio Fix +? +Audio Fixed +: other: Audio Fixed + +? +SyncFix +? +SyncFixed +? +Sync Fix +? +Sync Fixed +: other: Sync Fixed + +? +DualAudio +? +Dual Audio +: other: Dual Audio + +? +ws +? +WideScreen +? +Wide Screen +: other: Widescreen + +# Fix and Real must be surround by others properties to be matched. +? DVD.Real.XViD +? DVD.fix.XViD +? -DVD.Real +? -DVD.Fix +? -Real.XViD +? -Fix.XViD +: other: Proper + proper_count: 1 + +? -DVD.BlablaBla.Fix.Blablabla.XVID +? -DVD.BlablaBla.Fix.XVID +? -DVD.Fix.Blablabla.XVID +: other: Proper + proper_count: 1 + + +? DVD.Real.PROPER.REPACK +: other: Proper + proper_count: 3 + + +? Proper +? +Repack +? +Rerip +: other: Proper + proper_count: 1 + +? XViD.Fansub +: other: Fan Subtitled + +? XViD.Fastsub +: other: Fast Subtitled + +? +Season Complete +? -Complete +: other: Complete + +? R5 +: other: Region 5 + +? RC +: other: Region C + +? PreAir +? Pre Air +: other: Preair + +? Screener +: other: Screener + +? Remux +: other: Remux + +? 3D +: other: 3D + +? HD +: other: HD + +? FHD +? FullHD +? Full HD +: other: Full HD + +? UHD +? Ultra +? UltraHD +? Ultra HD +: other: Ultra HD + +? mHD # ?? +? HDLight +: other: Micro HD + +? HQ +: other: High Quality + +? hr +: other: High Resolution + +? PAL +: other: PAL + +? SECAM +: other: SECAM + +? NTSC +: other: NTSC + +? LDTV +: other: Low Definition + +? LD +: other: Line Dubbed + +? MD +: other: Mic Dubbed + +? -The complete movie +: other: Complete + +? +The complete movie +: title: The complete movie + +? +AC3-HQ +: audio_profile: High Quality + +? Other-HQ +: other: High Quality + +? reenc +? re-enc +? re-encoded +? reencoded +: other: Reencoded + +? CONVERT XViD +: other: Converted + +? +HDRIP # it's a Rip from non specified HD source +: other: [HD, Rip] + +? SDR +: other: Standard Dynamic Range + +? HDR +? HDR10 +? -HDR100 +: other: HDR10 + +? BT2020 +? BT.2020 +? -BT.20200 +? -BT.2021 +: other: BT.2020 + +? Upscaled +? Upscale +: other: Upscaled + diff --git a/libs/guessit/test/rules/part.yml b/libs/guessit/test/rules/part.yml new file mode 100644 index 000000000..72f3d98a8 --- /dev/null +++ b/libs/guessit/test/rules/part.yml @@ -0,0 +1,18 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Filename Part 3.mkv +? Filename Part III.mkv +? Filename Part Three.mkv +? Filename Part Trois.mkv +: title: Filename + part: 3 + +? Part 3 +? Part III +? Part Three +? Part Trois +? Part3 +: part: 3 + +? -Something.Apt.1 +: part: 1
\ No newline at end of file diff --git a/libs/guessit/test/rules/processors.yml b/libs/guessit/test/rules/processors.yml new file mode 100644 index 000000000..ee906b2c3 --- /dev/null +++ b/libs/guessit/test/rules/processors.yml @@ -0,0 +1,8 @@ +# Multiple input strings having same expected results can be chained. +# Use $ marker to check inputs that should not match results. + +# Prefer information for last path. +? Some movie (2000)/Some movie (2001).mkv +? Some movie (2001)/Some movie.mkv +: year: 2001 + container: mkv diff --git a/libs/guessit/test/rules/processors_test.py b/libs/guessit/test/rules/processors_test.py new file mode 100644 index 000000000..c22e968ce --- /dev/null +++ b/libs/guessit/test/rules/processors_test.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name, pointless-string-statement + +from rebulk.match import Matches, Match + +from ...rules.processors import StripSeparators + + +def test_strip_separators(): + strip_separators = StripSeparators() + + matches = Matches() + + m = Match(3, 11, input_string="pre.ABCDEF.post") + + assert m.raw == '.ABCDEF.' + matches.append(m) + + returned_matches = strip_separators.when(matches, None) + assert returned_matches == matches + + strip_separators.then(matches, returned_matches, None) + + assert m.raw == 'ABCDEF' + + +def test_strip_separators_keep_acronyms(): + strip_separators = StripSeparators() + + matches = Matches() + + m = Match(0, 13, input_string=".S.H.I.E.L.D.") + m2 = Match(0, 22, input_string=".Agent.Of.S.H.I.E.L.D.") + + assert m.raw == '.S.H.I.E.L.D.' + matches.append(m) + matches.append(m2) + + returned_matches = strip_separators.when(matches, None) + assert returned_matches == matches + + strip_separators.then(matches, returned_matches, None) + + assert m.raw == '.S.H.I.E.L.D.' + assert m2.raw == 'Agent.Of.S.H.I.E.L.D.' diff --git a/libs/guessit/test/rules/release_group.yml b/libs/guessit/test/rules/release_group.yml new file mode 100644 index 000000000..c96383e94 --- /dev/null +++ b/libs/guessit/test/rules/release_group.yml @@ -0,0 +1,71 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Some.Title.XViD-ReleaseGroup +? Some.Title.XViD-ReleaseGroup.mkv +: release_group: ReleaseGroup + +? Some.Title.XViD-by.Artik[SEDG].avi +: release_group: Artik[SEDG] + +? "[ABC] Some.Title.avi" +? some/folder/[ABC]Some.Title.avi +: release_group: ABC + +? "[ABC] Some.Title.XViD-GRP.avi" +? some/folder/[ABC]Some.Title.XViD-GRP.avi +: release_group: GRP + +? "[ABC] Some.Title.S01E02.avi" +? some/folder/[ABC]Some.Title.S01E02.avi +: release_group: ABC + +? Some.Title.XViD-S2E02.NoReleaseGroup.avi +: release_group: !!null + +? Test.S01E01-FooBar-Group +: options: -G group -G xxxx + episode: 1 + episode_title: FooBar + release_group: Group + season: 1 + title: Test + type: episode + +? Test.S01E01-FooBar-Group +: options: -G re:gr.?up -G xxxx + episode: 1 + episode_title: FooBar + release_group: Group + season: 1 + title: Test + type: episode + +? Show.Name.x264-byEMP +: title: Show Name + video_codec: H.264 + release_group: byEMP + +? Show.Name.x264-NovaRip +: title: Show Name + video_codec: H.264 + release_group: NovaRip + +? Show.Name.x264-PARTiCLE +: title: Show Name + video_codec: H.264 + release_group: PARTiCLE + +? Show.Name.x264-POURMOi +: title: Show Name + video_codec: H.264 + release_group: POURMOi + +? Show.Name.x264-RipPourBox +: title: Show Name + video_codec: H.264 + release_group: RipPourBox + +? Show.Name.x264-RiPRG +: title: Show Name + video_codec: H.264 + release_group: RiPRG diff --git a/libs/guessit/test/rules/screen_size.yml b/libs/guessit/test/rules/screen_size.yml new file mode 100644 index 000000000..450d77e4e --- /dev/null +++ b/libs/guessit/test/rules/screen_size.yml @@ -0,0 +1,259 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +360p +? +360px +? -360 +? +500x360 +? -250x360 +: screen_size: 360p + +? +640x360 +? -640x360i +? -684x360i +: screen_size: 360p + aspect_ratio: 1.778 + +? +360i +: screen_size: 360i + +? +480x360i +? -480x360p +? -450x360 +: screen_size: 360i + aspect_ratio: 1.333 + +? +368p +? +368px +? -368i +? -368 +? +500x368 +: screen_size: 368p + +? -490x368 +? -700x368 +: screen_size: 368p + +? +492x368p +: screen_size: + aspect_ratio: 1.337 + +? +654x368 +: screen_size: 368p + aspect_ratio: 1.777 + +? +698x368 +: screen_size: 368p + aspect_ratio: 1.897 + +? +368i +: -screen_size: 368i + +? +480p +? +480px +? -480i +? -480 +? -500x480 +? -638x480 +? -920x480 +: screen_size: 480p + +? +640x480 +: screen_size: 480p + aspect_ratio: 1.333 + +? +852x480 +: screen_size: 480p + aspect_ratio: 1.775 + +? +910x480 +: screen_size: 480p + aspect_ratio: 1.896 + +? +500x480 +? +500 x 480 +? +500 * 480 +? +500x480p +? +500X480i +: screen_size: 500x480 + aspect_ratio: 1.042 + +? +480i +? +852x480i +: screen_size: 480i + +? +576p +? +576px +? -576i +? -576 +? -500x576 +? -766x576 +? -1094x576 +: screen_size: 576p + +? +768x576 +: screen_size: 576p + aspect_ratio: 1.333 + +? +1024x576 +: screen_size: 576p + aspect_ratio: 1.778 + +? +1092x576 +: screen_size: 576p + aspect_ratio: 1.896 + +? +500x576 +: screen_size: 500x576 + aspect_ratio: 0.868 + +? +576i +: screen_size: 576i + +? +720p +? +720px +? -720i +? 720hd +? 720pHD +? -720 +? -500x720 +? -950x720 +? -1368x720 +: screen_size: 720p + +? +960x720 +: screen_size: 720p + aspect_ratio: 1.333 + +? +1280x720 +: screen_size: 720p + aspect_ratio: 1.778 + +? +1366x720 +: screen_size: 720p + aspect_ratio: 1.897 + +? +500x720 +: screen_size: 500x720 + aspect_ratio: 0.694 + +? +900p +? +900px +? -900i +? -900 +? -500x900 +? -1198x900 +? -1710x900 +: screen_size: 900p + +? +1200x900 +: screen_size: 900p + aspect_ratio: 1.333 + +? +1600x900 +: screen_size: 900p + aspect_ratio: 1.778 + +? +1708x900 +: screen_size: 900p + aspect_ratio: 1.898 + +? +500x900 +? +500x900p +? +500x900i +: screen_size: 500x900 + aspect_ratio: 0.556 + +? +900i +: screen_size: 900i + +? +1080p +? +1080px +? +1080hd +? +1080pHD +? -1080i +? -1080 +? -500x1080 +? -1438x1080 +? -2050x1080 +: screen_size: 1080p + +? +1440x1080 +: screen_size: 1080p + aspect_ratio: 1.333 + +? +1920x1080 +: screen_size: 1080p + aspect_ratio: 1.778 + +? +2048x1080 +: screen_size: 1080p + aspect_ratio: 1.896 + +? +1080i +? -1080p +: screen_size: 1080i + +? 1440p +: screen_size: 1440p + +? +500x1080 +: screen_size: 500x1080 + aspect_ratio: 0.463 + +? +2160p +? +2160px +? -2160i +? -2160 +? +4096x2160 +? +4k +? -2878x2160 +? -4100x2160 +: screen_size: 2160p + +? +2880x2160 +: screen_size: 2160p + aspect_ratio: 1.333 + +? +3840x2160 +: screen_size: 2160p + aspect_ratio: 1.778 + +? +4098x2160 +: screen_size: 2160p + aspect_ratio: 1.897 + +? +500x2160 +: screen_size: 500x2160 + aspect_ratio: 0.231 + +? +4320p +? +4320px +? -4320i +? -4320 +? -5758x2160 +? -8198x2160 +: screen_size: 4320p + +? +5760x4320 +: screen_size: 4320p + aspect_ratio: 1.333 + +? +7680x4320 +: screen_size: 4320p + aspect_ratio: 1.778 + +? +8196x4320 +: screen_size: 4320p + aspect_ratio: 1.897 + +? +500x4320 +: screen_size: 500x4320 + aspect_ratio: 0.116 + +? Test.File.720hd.bluray +? Test.File.720p24 +? Test.File.720p30 +? Test.File.720p50 +? Test.File.720p60 +? Test.File.720p120 +: screen_size: 720p diff --git a/libs/guessit/test/rules/size.yml b/libs/guessit/test/rules/size.yml new file mode 100644 index 000000000..18b3cd494 --- /dev/null +++ b/libs/guessit/test/rules/size.yml @@ -0,0 +1,8 @@ +? 1.1tb +: size: 1.1TB + +? 123mb +: size: 123MB + +? 4.3gb +: size: 4.3GB diff --git a/libs/guessit/test/rules/source.yml b/libs/guessit/test/rules/source.yml new file mode 100644 index 000000000..cda8f1ac4 --- /dev/null +++ b/libs/guessit/test/rules/source.yml @@ -0,0 +1,323 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +VHS +? -VHSAnythingElse +? -SomeVHS stuff +? -VH +? -VHx +: source: VHS + -other: Rip + +? +VHSRip +? +VHS-Rip +? +VhS_rip +? +VHS.RIP +? -VHS +? -VHxRip +: source: VHS + other: Rip + +? +Cam +: source: Camera + -other: Rip + +? +CamRip +? +CaM Rip +? +Cam_Rip +? +cam.rip +? -Cam +: source: Camera + other: Rip + +? +HDCam +? +HD-Cam +: source: HD Camera + -other: Rip + +? +HDCamRip +? +HD-Cam.rip +? -HDCam +? -HD-Cam +: source: HD Camera + other: Rip + +? +Telesync +? +TS +: source: Telesync + -other: Rip + +? +TelesyncRip +? +TSRip +? -Telesync +? -TS +: source: Telesync + other: Rip + +? +HD TS +? -Hd.Ts # ts file extension +? -HD.TS # ts file extension +? +Hd-Ts +: source: HD Telesync + -other: Rip + +? +HD TS Rip +? +Hd-Ts-Rip +? -HD TS +? -Hd-Ts +: source: HD Telesync + other: Rip + +? +Workprint +? +workPrint +? +WorkPrint +? +WP +? -Work Print +: source: Workprint + -other: Rip + +? +Telecine +? +teleCine +? +TC +? -Tele Cine +: source: Telecine + -other: Rip + +? +Telecine Rip +? +teleCine-Rip +? +TC-Rip +? -Telecine +? -TC +: source: Telecine + other: Rip + +? +HD-TELECINE +? +HDTC +: source: HD Telecine + -other: Rip + +? +HD-TCRip +? +HD TELECINE RIP +? -HD-TELECINE +? -HDTC +: source: HD Telecine + other: Rip + +? +PPV +: source: Pay-per-view + -other: Rip + +? +ppv-rip +? -PPV +: source: Pay-per-view + other: Rip + +? -TV +? +SDTV +? +TV-Dub +: source: TV + -other: Rip + +? +SDTVRIP +? +Rip sd tv +? +TvRip +? +Rip TV +? -TV +? -SDTV +: source: TV + other: Rip + +? +DVB +? +pdTV +? +Pd Tv +: source: Digital TV + -other: Rip + +? +DVB-Rip +? +DvBRiP +? +pdtvRiP +? +pd tv RiP +? -DVB +? -pdTV +? -Pd Tv +: source: Digital TV + other: Rip + +? +DVD +? +video ts +? +DVDR +? +DVD 9 +? +dvd 5 +? -dvd ts +: source: DVD + -source: Telesync + -other: Rip + +? +DVD-RIP +? -video ts +? -DVD +? -DVDR +? -DVD 9 +? -dvd 5 +: source: DVD + other: Rip + +? +HDTV +: source: HDTV + -other: Rip + +? +tv rip hd +? +HDtv Rip +? -HdRip # it's a Rip from non specified HD source +? -HDTV +: source: HDTV + other: Rip + +? +VOD +: source: Video on Demand + -other: Rip + +? +VodRip +? +vod rip +? -VOD +: source: Video on Demand + other: Rip + +? +webrip +? +Web Rip +? +webdlrip +? +web dl rip +? +webcap +? +web cap +? +webcaprip +? +web cap rip +: source: Web + other: Rip + +? +webdl +? +Web DL +? +webHD +? +WEB hd +? +web +: source: Web + -other: Rip + +? +HDDVD +? +hd dvd +: source: HD-DVD + -other: Rip + +? +hdDvdRip +? -HDDVD +? -hd dvd +: source: HD-DVD + other: Rip + +? +BluRay +? +BD +? +BD5 +? +BD9 +? +BD25 +? +bd50 +: source: Blu-ray + -other: Rip + +? +BR-Scr +? +BR.Screener +: source: Blu-ray + other: [Reencoded, Screener] + -language: pt-BR + +? +BR-Rip +? +BRRip +: source: Blu-ray + other: [Reencoded, Rip] + -language: pt-BR + +? +BluRay rip +? +BDRip +? -BluRay +? -BD +? -BR +? -BR rip +? -BD5 +? -BD9 +? -BD25 +? -bd50 +: source: Blu-ray + other: Rip + +? XVID.NTSC.DVDR.nfo +: source: DVD + -other: Rip + +? +AHDTV +: source: Analog HDTV + -other: Rip + +? +dsr +? +dth +: source: Satellite + -other: Rip + +? +dsrip +? +ds rip +? +dsrrip +? +dsr rip +? +satrip +? +sat rip +? +dthrip +? +dth rip +? -dsr +? -dth +: source: Satellite + other: Rip + +? +UHDTV +: source: Ultra HDTV + -other: Rip + +? +UHDRip +? +UHDTV Rip +? -UHDTV +: source: Ultra HDTV + other: Rip + +? UHD Bluray +? UHD 2160p Bluray +? UHD 8bit Bluray +? UHD HQ 8bit Bluray +? Ultra Bluray +? Ultra HD Bluray +? Bluray ULTRA +? Bluray Ultra HD +? Bluray UHD +? 4K Bluray +? 2160p Bluray +? UHD 10bit HDR Bluray +? UHD HDR10 Bluray +? -HD Bluray +? -AMERICAN ULTRA (2015) 1080p Bluray +? -American.Ultra.2015.BRRip +? -BRRip XviD AC3-ULTRAS +? -UHD Proper Bluray +: source: Ultra HD Blu-ray + +? UHD.BRRip +? UHD.2160p.BRRip +? BRRip.2160p.UHD +? BRRip.[4K-2160p-UHD] +: source: Ultra HD Blu-ray + other: [Reencoded, Rip] + +? UHD.2160p.BDRip +? BDRip.[4K-2160p-UHD] +: source: Ultra HD Blu-ray + other: Rip + +? DM +: source: Digital Master + +? DMRIP +? DM-RIP +: source: Digital Master + other: Rip diff --git a/libs/guessit/test/rules/title.yml b/libs/guessit/test/rules/title.yml new file mode 100644 index 000000000..fffaf8a25 --- /dev/null +++ b/libs/guessit/test/rules/title.yml @@ -0,0 +1,32 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? Title Only +? -Title XViD 720p Only +? sub/folder/Title Only +? -sub/folder/Title XViD 720p Only +? Title Only.mkv +? Title Only.avi +: title: Title Only + +? Title Only/title_only.mkv +: title: Title Only + +? title_only.mkv +: title: title only + +? Some Title/some.title.mkv +? some.title/Some.Title.mkv +: title: Some Title + +? SOME TITLE/Some.title.mkv +? Some.title/SOME TITLE.mkv +: title: Some title + +? some title/Some.title.mkv +? Some.title/some title.mkv +: title: Some title + +? Some other title/Some.Other.title.mkv +? Some.Other title/Some other title.mkv +: title: Some Other title + diff --git a/libs/guessit/test/rules/video_codec.yml b/libs/guessit/test/rules/video_codec.yml new file mode 100644 index 000000000..c37c1a603 --- /dev/null +++ b/libs/guessit/test/rules/video_codec.yml @@ -0,0 +1,87 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? rv10 +? rv13 +? RV20 +? Rv30 +? rv40 +? -xrv40 +: video_codec: RealVideo + +? mpeg2 +? MPEG2 +? MPEG-2 +? mpg2 +? H262 +? H.262 +? x262 +? -mpeg +? -xmpeg2 +? -mpeg2x +: video_codec: MPEG-2 + +? DivX +? -div X +? divx +? dvdivx +? DVDivX +: video_codec: DivX + +? XviD +? xvid +? -x vid +: video_codec: Xvid + +? h263 +? x263 +? h.263 +: video_codec: H.263 + +? h264 +? x264 +? h.264 +? x.264 +? mpeg4-AVC +? AVC +? AVCHD +? AVCHD-SC +? H.264-SC +? H.264-AVCHD-SC +? -MPEG-4 +? -mpeg4 +? -mpeg +? -h 265 +? -x265 +: video_codec: H.264 + +? h265 +? x265 +? h.265 +? x.265 +? hevc +? -h 264 +? -x264 +: video_codec: H.265 + +? hevc10 +? HEVC-YUV420P10 +: video_codec: H.265 + color_depth: 10-bit + +? h265-HP +: video_codec: H.265 + video_profile: High + +? VC1 +? VC-1 +: video_codec: VC-1 + +? VP7 +: video_codec: VP7 + +? VP8 +? VP80 +: video_codec: VP8 + +? VP9 +: video_codec: VP9 diff --git a/libs/guessit/test/rules/website.yml b/libs/guessit/test/rules/website.yml new file mode 100644 index 000000000..11d434d2a --- /dev/null +++ b/libs/guessit/test/rules/website.yml @@ -0,0 +1,23 @@ +# Multiple input strings having same expected results can be chained. +# Use - marker to check inputs that should not match results. +? +tvu.org.ru +? -tvu.unsafe.ru +: website: tvu.org.ru + +? +www.nimp.na +? -somewww.nimp.na +? -www.nimp.nawouak +? -nimp.na +: website: www.nimp.na + +? +wawa.co.uk +? -wawa.uk +: website: wawa.co.uk + +? -Dark.Net.S01E06.720p.HDTV.x264-BATV + -Dark.Net.2015.720p.HDTV.x264-BATV +: website: Dark.Net + +? Dark.Net.S01E06.720p.HDTV.x264-BATV + Dark.Net.2015.720p.HDTV.x264-BATV +: title: Dark Net diff --git a/libs/guessit/test/streaming_services.yaml b/libs/guessit/test/streaming_services.yaml new file mode 100644 index 000000000..adf52e715 --- /dev/null +++ b/libs/guessit/test/streaming_services.yaml @@ -0,0 +1,1934 @@ +? House.of.Cards.2013.S02E03.1080p.NF.WEBRip.DD5.1.x264-NTb.mkv +? House.of.Cards.2013.S02E03.1080p.Netflix.WEBRip.DD5.1.x264-NTb.mkv +: title: House of Cards + year: 2013 + season: 2 + episode: 3 + screen_size: 1080p + streaming_service: Netflix + source: Web + other: Rip + audio_channels: "5.1" + audio_codec: Dolby Digital + video_codec: H.264 + release_group: NTb + +? The.Daily.Show.2015.07.01.Kirsten.Gillibrand.Extended.720p.CC.WEBRip.AAC2.0.x264-BTW.mkv +? The.Daily.Show.2015.07.01.Kirsten.Gillibrand.Extended.720p.ComedyCentral.WEBRip.AAC2.0.x264-BTW.mkv +? The.Daily.Show.2015.07.01.Kirsten.Gillibrand.Extended.720p.Comedy.Central.WEBRip.AAC2.0.x264-BTW.mkv +: audio_channels: '2.0' + audio_codec: AAC + date: 2015-07-01 + edition: Extended + source: Web + other: Rip + release_group: BTW + screen_size: 720p + streaming_service: Comedy Central + title: The Daily Show + episode_title: Kirsten Gillibrand + video_codec: H.264 + +? The.Daily.Show.2015.07.01.Kirsten.Gillibrand.Extended.Interview.720p.CC.WEBRip.AAC2.0.x264-BTW.mkv +: audio_channels: '2.0' + audio_codec: AAC + date: 2015-07-01 + source: Web + release_group: BTW + screen_size: 720p + streaming_service: Comedy Central + title: The Daily Show + episode_title: Kirsten Gillibrand Extended Interview + video_codec: H.264 + +? The.Daily.Show.2015.07.02.Sarah.Vowell.CC.WEBRip.AAC2.0.x264-BTW.mkv +: audio_channels: '2.0' + audio_codec: AAC + date: 2015-07-02 + source: Web + release_group: BTW + streaming_service: Comedy Central + title: The Daily Show + episode_title: Sarah Vowell + video_codec: H.264 + +# Streaming service: Amazon +? Show.Name.S07E04.Service.1080p.AMZN.WEBRip.DD+5.1.x264 +? Show.Name.S07E04.Service.1080p.AmazonPrime.WEBRip.DD+5.1.x264 +: title: Show Name + season: 7 + episode: 4 + episode_title: Service + screen_size: 1080p + streaming_service: Amazon Prime + source: Web + other: Rip + audio_codec: Dolby Digital Plus + audio_channels: '5.1' + video_codec: H.264 + type: episode + +# Streaming service: Comedy Central +? Show.Name.2016.09.28.Nice.Title.Extended.1080p.CC.WEBRip.AAC2.0.x264-monkee +: title: Show Name + date: 2016-09-28 + episode_title: Nice Title + edition: Extended + other: Rip + screen_size: 1080p + streaming_service: Comedy Central + source: Web + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: The CW +? Show.Name.US.S12E20.Nice.Title.720p.CW.WEBRip.AAC2.0.x264-monkee +? Show.Name.US.S12E20.Nice.Title.720p.TheCW.WEBRip.AAC2.0.x264-monkee +: title: Show Name + country: US + season: 12 + episode: 20 + episode_title: Nice Title + screen_size: 720p + streaming_service: The CW + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: AMBC +? Show.Name.2016.09.27.Nice.Title.720p.AMBC.WEBRip.AAC2.0.x264-monkee +: title: Show Name + date: 2016-09-27 + episode_title: Nice Title + screen_size: 720p + streaming_service: ABC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: HIST +? Show.Name.720p.HIST.WEBRip.AAC2.0.H.264-monkee +? Show.Name.720p.History.WEBRip.AAC2.0.H.264-monkee +: options: -t episode + title: Show Name + screen_size: 720p + streaming_service: History + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: PBS +? Show.Name.2015.Nice.Title.1080p.PBS.WEBRip.AAC2.0.H264-monkee +: options: -t episode + title: Show Name + year: 2015 + episode_title: Nice Title + screen_size: 1080p + streaming_service: PBS + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: SeeSo +? Show.Name.2016.Nice.Title.1080p.SESO.WEBRip.AAC2.0.x264-monkee +: options: -t episode + title: Show Name + year: 2016 + episode_title: Nice Title + screen_size: 1080p + streaming_service: SeeSo + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Discovery +? Show.Name.S01E03.Nice.Title.720p.DISC.WEBRip.AAC2.0.x264-NTb +? Show.Name.S01E03.Nice.Title.720p.Discovery.WEBRip.AAC2.0.x264-NTb +: title: Show Name + season: 1 + episode: 3 + episode_title: Nice Title + screen_size: 720p + streaming_service: Discovery + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: NTb + type: episode + +# Streaming service: BBC iPlayer +? Show.Name.2016.08.18.Nice.Title.720p.iP.WEBRip.AAC2.0.H.264-monkee +? Show.Name.2016.08.18.Nice.Title.720p.BBCiPlayer.WEBRip.AAC2.0.H.264-monkee +: title: Show Name + date: 2016-08-18 + episode_title: Nice Title + streaming_service: BBC iPlayer + screen_size: 720p + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: A&E +? Show.Name.S15E18.Nice.Title.720p.AE.WEBRip.AAC2.0.H.264-monkee +? Show.Name.S15E18.Nice.Title.720p.A&E.WEBRip.AAC2.0.H.264-monkee +: title: Show Name + season: 15 + episode: 18 + episode_title: Nice Title + screen_size: 720p + streaming_service: A&E + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Adult Swim +? Show.Name.S04E01.Nice.Title.1080p.AS.WEBRip.AAC2.0.H.264-monkee +? Show.Name.S04E01.Nice.Title.1080p.AdultSwim.WEBRip.AAC2.0.H.264-monkee +: title: Show Name + season: 4 + episode: 1 + episode_title: Nice Title + screen_size: 1080p + streaming_service: Adult Swim + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Netflix +? Show.Name.2013.S02E03.1080p.NF.WEBRip.DD5.1.x264-NTb.mkv +: title: Show Name + year: 2013 + season: 2 + episode: 3 + screen_size: 1080p + streaming_service: Netflix + source: Web + other: Rip + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: NTb + container: mkv + type: episode + +# Streaming service: CBS +? Show.Name.2016.05.10.Nice.Title.720p.CBS.WEBRip.AAC2.0.x264-monkee +: title: Show Name + date: 2016-05-10 + episode_title: Nice Title + screen_size: 720p + streaming_service: CBS + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: NBA TV +? NBA.2016.02.27.Team.A.vs.Team.B.720p.NBA.WEBRip.AAC2.0.H.264-monkee +? NBA.2016.02.27.Team.A.vs.Team.B.720p.NBATV.WEBRip.AAC2.0.H.264-monkee +: title: NBA + date: 2016-02-27 + episode_title: Team A vs Team B + screen_size: 720p + streaming_service: NBA TV + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: ePix +? Show.Name.S05E04.Nice.Title.Part4.720p.EPIX.WEBRip.AAC2.0.H.264-monkee +? Show.Name.S05E04.Nice.Title.Part4.720p.ePix.WEBRip.AAC2.0.H.264-monkee +: title: Show Name + season: 5 + episode: 4 + episode_title: Nice Title + part: 4 + screen_size: 720p + streaming_service: ePix + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: NBC +? Show.Name.S41E03.Nice.Title.720p.NBC.WEBRip.AAC2.0.x264-monkee +: title: Show Name + season: 41 + episode: 3 + episode_title: Nice Title + screen_size: 720p + streaming_service: NBC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Syfy +? Show.Name.S01E02.Nice.Title.720p.SYFY.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.Syfy.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: Syfy + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: Spike TV +? Show.Name.S01E02.Nice.Title.720p.SPKE.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.Spike TV.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.SpikeTV.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: Spike TV + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: IFC +? Show.Name.S01E02.Nice.Title.720p.IFC.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: IFC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: NATG +? Show.Name.S01E02.Nice.Title.720p.NATG.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.NationalGeographic.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: National Geographic + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: NFL +? Show.Name.S01E02.Nice.Title.720p.NFL.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: NFL + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: UFC +? Show.Name.S01E02.Nice.Title.720p.UFC.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: UFC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: TV Land +? Show.Name.S01E02.Nice.Title.720p.TVL.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.TVLand.WEBRip.AAC2.0.x264-group +? Show.Name.S01E02.Nice.Title.720p.TV Land.WEBRip.AAC2.0.x264-group +: title: Show Name + season: 1 + episode: 2 + episode_title: Nice Title + screen_size: 720p + streaming_service: TV Land + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: group + type: episode + +# Streaming service: Crunchy Roll +? Show.Name.S01.1080p.CR.WEBRip.AAC.2.0.x264-monkee +: title: Show Name + season: 1 + screen_size: 1080p + streaming_service: Crunchy Roll + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Disney +? Show.Name.S01.1080p.DSNY.WEBRip.AAC.2.0.x264-monkee +? Show.Name.S01.1080p.Disney.WEBRip.AAC.2.0.x264-monkee +: title: Show Name + season: 1 + screen_size: 1080p + streaming_service: Disney + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: Nickelodeon +? Show.Name.S01.1080p.NICK.WEBRip.AAC.2.0.x264-monkee +? Show.Name.S01.1080p.Nickelodeon.WEBRip.AAC.2.0.x264-monkee +: title: Show Name + season: 1 + screen_size: 1080p + streaming_service: Nickelodeon + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: TFou +? Show.Name.S01.1080p.TFOU.WEBRip.AAC.2.0.x264-monkee +? Show.Name.S01.1080p.TFou.WEBRip.AAC.2.0.x264-monkee +: title: Show Name + season: 1 + screen_size: 1080p + streaming_service: TFou + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: monkee + type: episode + +# Streaming service: DIY Network +? Show.Name.S01.720p.DIY.WEBRip.AAC2.0.H.264-BTN +: title: Show Name + season: 1 + screen_size: 720p + streaming_service: DIY Network + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTN + type: episode + +# Streaming service: USA Network +? Show.Name.S01E02.Exfil.1080p.USAN.WEBRip.AAC2.0.x264-AJP69 +: title: Show Name + season: 1 + episode: 2 + screen_size: 1080p + streaming_service: USA Network + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: AJP69 + type: episode + +# Streaming service: TV3 Ireland +? Show.Name.S01E08.576p.TV3.WEBRip.AAC2.0.x264-HARiKEN +: title: Show Name + season: 1 + episode: 8 + screen_size: 576p + streaming_service: TV3 Ireland + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: HARiKEN + type: episode + +# Streaming service: TV4 Sweeden +? Show.Name.S05.720p.TV4.WEBRip.AAC2.0.H.264-BTW +: title: Show Name + season: 5 + screen_size: 720p + streaming_service: TV4 Sweeden + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTW + type: episode + +# Streaming service: TLC +? Show.Name.S02.720p.TLC.WEBRip.AAC2.0.x264-BTW +: title: Show Name + season: 2 + screen_size: 720p + streaming_service: TLC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTW + type: episode + +# Streaming service: Investigation Discovery +? Show.Name.S01E01.720p.ID.WEBRip.AAC2.0.x264-BTW +: title: Show Name + season: 1 + episode: 1 + screen_size: 720p + streaming_service: Investigation Discovery + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: BTW + type: episode + +# Streaming service: RTÉ One +? Show.Name.S10E01.576p.RTE.WEBRip.AAC2.0.H.264-RTN +: title: Show Name + season: 10 + episode: 1 + screen_size: 576p + streaming_service: RTÉ One + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: RTN + type: episode + +# Streaming service: AMC +? Show.Name.S01E01.1080p.AMC.WEBRip.H.264.AAC2.0-CasStudio +: title: Show Name + season: 1 + episode: 1 + screen_size: 1080p + streaming_service: AMC + source: Web + other: Rip + audio_codec: AAC + audio_channels: '2.0' + video_codec: H.264 + release_group: CasStudio + type: episode + +? Suits.S07E01.1080p.iT.WEB-DL.DD5.1.H.264-VLAD.mkv +? Suits.S07E01.1080p.iTunes.WEB-DL.DD5.1.H.264-VLAD.mkv +: title: Suits + season: 7 + episode: 1 + screen_size: 1080p + source: Web + streaming_service: iTunes + audio_codec: Dolby Digital + audio_channels: '5.1' + video_codec: H.264 + release_group: VLAD + container: mkv + type: episode + +? UpFront.S01.720p.AJAZ.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + season: 1 + source: Web + streaming_service: Al Jazeera English + title: UpFront + type: episode + video_codec: H.264 + +? Smack.The.Pony.S01.4OD.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + season: 1 + source: Web + streaming_service: Channel 4 + title: Smack The Pony + type: episode + video_codec: H.264 + +? The.Toy.Box.S01E01.720p.AMBC.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: BTN + screen_size: 720p + season: 1 + source: Web + streaming_service: ABC + title: The Toy Box + type: episode + video_codec: H.264 + +? Gundam.Reconguista.in.G.S01.720p.ANLB.WEBRip.AAC2.0.x264-HorribleSubs +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: HorribleSubs + screen_size: 720p + season: 1 + source: Web + streaming_service: AnimeLab + title: Gundam Reconguista in G + type: episode + video_codec: H.264 + +? Animal.Nation.with.Anthony.Anderson.S01E01.1080p.ANPL.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: RTN + screen_size: 1080p + season: 1 + source: Web + streaming_service: Animal Planet + title: Animal Nation with Anthony Anderson + type: episode + video_codec: H.264 + +? Park.Bench.S01.1080p.AOL.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 1080p + season: 1 + source: Web + streaming_service: AOL + title: Park Bench + type: episode + video_codec: H.264 + +? Crime.Scene.Cleaner.S05.720p.ARD.WEBRip.AAC2.0.H.264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + screen_size: 720p + season: 5 + source: Web + streaming_service: ARD + title: Crime Scene Cleaner + type: episode + video_codec: H.264 + +? Decker.S03.720p.AS.WEB-DL.AAC2.0.H.264-RTN +: audio_channels: '2.0' + audio_codec: AAC + release_group: RTN + screen_size: 720p + season: 3 + source: Web + streaming_service: Adult Swim + title: Decker + type: episode + video_codec: H.264 + +? Southern.Charm.Savannah.S01E04.Hurricane.On.The.Horizon.1080p.BRAV.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + episode: 4 + episode_title: Hurricane On The Horizon + other: Rip + release_group: BTW + screen_size: 1080p + season: 1 + source: Web + streaming_service: BravoTV + title: Southern Charm Savannah + type: episode + video_codec: H.264 + +? Four.in.the.Morning.S01E01.Pig.RERip.720p.CBC.WEBRip.AAC2.0.H.264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + episode_title: Pig + other: + - Proper + - Rip + proper_count: 1 + release_group: RTN + screen_size: 720p + season: 1 + source: Web + streaming_service: CBC + title: Four in the Morning + type: episode + video_codec: H.264 + +? Rio.Olympics.2016.08.07.Mens.Football.Group.C.Germany.vs.South.Korea.720p.CBC.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + date: 2016-08-07 + episode_title: Mens Football Group C Germany vs South Korea + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: CBC + title: Rio Olympics + type: episode + video_codec: H.264 + +? Comedians.In.Cars.Getting.Coffee.S01.720p.CCGC.WEBRip.AAC2.0.x264-monkee +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: monkee + screen_size: 720p + season: 1 + source: Web + streaming_service: Comedians in Cars Getting Coffee + title: Comedians In Cars Getting Coffee + type: episode + video_codec: H.264 + +? Life.on.Top.S02.720p.CMAX.WEBRip.AAC2.0.x264-CMAX +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: CMAX + screen_size: 720p + season: 2 + source: Web + streaming_service: Cinemax + title: Life on Top + type: episode + video_codec: H.264 + +? Sun.Records.S01.720p.CMT.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + season: 1 + source: Web + streaming_service: Country Music Television + title: Sun Records + type: episode + video_codec: H.264 + +? Infinity.Train.S01E00.Pilot.REPACK.720p.CN.WEBRip.AAC2.0.H.264-monkee +: audio_channels: '2.0' + audio_codec: AAC + episode: 0 + episode_details: Pilot + episode_title: Pilot + language: zh + other: + - Proper + - Rip + proper_count: 1 + release_group: monkee + screen_size: 720p + season: 1 + source: Web + streaming_service: Cartoon Network + title: Infinity Train + type: episode + video_codec: H.264 + +? Jay.Lenos.Garage.2015.S03E02.1080p.CNBC.WEB-DL.x264-TOPKEK +: episode: 2 + release_group: TOPKEK + screen_size: 1080p + season: 3 + source: Web + streaming_service: CNBC + title: Jay Lenos Garage + type: episode + video_codec: H.264 + year: 2015 + +? US.Presidential.Debates.2015.10.28.Third.Republican.Debate.720p.CNBC.WEBRip.AAC2.0.H.264-monkee +: audio_channels: '2.0' + audio_codec: AAC + country: US + date: 2015-10-28 + episode_title: Third Republican Debate + other: Rip + release_group: monkee + screen_size: 720p + source: Web + streaming_service: CNBC + title: Presidential Debates + type: episode + video_codec: H.264 + +? What.The.Fuck.France.S01E01.Le.doublage.CNLP.WEBRip.AAC2.0.x264-TURTLE +: audio_channels: '2.0' + audio_codec: AAC + country: FR + episode: 1 + episode_title: Le doublage + other: Rip + release_group: TURTLE + season: 1 + source: Web + streaming_service: Canal+ + title: What The Fuck + type: episode + video_codec: H.264 + +? SuperMansion.S02.720p.CRKL.WEBRip.AAC2.0.x264-VLAD +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: VLAD + screen_size: 720p + season: 2 + source: Web + streaming_service: Crackle + title: SuperMansion + type: episode + video_codec: H.264 + +? Chosen.S02.1080p.CRKL.WEBRip.AAC2.0.x264-AJP69 +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: AJP69 + screen_size: 1080p + season: 2 + source: Web + streaming_service: Crackle + title: Chosen + type: episode + video_codec: H.264 + +? Chosen.S03.1080p.CRKL.WEBRip.AAC2.0.x264-AJP69 +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: AJP69 + screen_size: 1080p + season: 3 + source: Web + streaming_service: Crackle + title: Chosen + type: episode + video_codec: H.264 + +? Snatch.S01.1080p.CRKL.WEBRip.AAC2.0.x264-DEFLATE +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: DEFLATE + screen_size: 1080p + season: 1 + source: Web + streaming_service: Crackle + title: Snatch + type: episode + video_codec: H.264 + +? White.House.Correspondents.Dinner.2015.Complete.CSPN.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: + - Complete + - Rip + release_group: BTW + source: Web + streaming_service: CSpan + title: White House Correspondents Dinner + type: movie + video_codec: H.264 + year: 2015 + +? The.Amazing.Race.Canada.S03.720p.CTV.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + country: CA + other: Rip + release_group: BTW + screen_size: 720p + season: 3 + source: Web + streaming_service: CTV + title: The Amazing Race + type: episode + video_codec: H.264 + +? Miniverse.S01E01.Explore.the.Solar.System.2160p.CUR.WEB-DL.DDP2.0.x264-monkee +: audio_channels: '2.0' + audio_codec: Dolby Digital Plus + episode: 1 + episode_title: Explore the Solar System + release_group: monkee + screen_size: 2160p + season: 1 + source: Web + streaming_service: CuriosityStream + title: Miniverse + type: episode + video_codec: H.264 + +? Vixen.S02.720p.CWS.WEBRip.AAC2.0.x264-BMF +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BMF + screen_size: 720p + season: 2 + source: Web + streaming_service: CWSeed + title: Vixen + type: episode + video_codec: H.264 + +? Abidin.Dino.DDY.WEBRip.AAC2.0.H.264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + source: Web + streaming_service: Digiturk Diledigin Yerde + title: Abidin Dino + type: movie + video_codec: H.264 + +? Fast.N.Loud.S08.1080p.DISC.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + screen_size: 1080p + season: 8 + source: Web + streaming_service: Discovery + title: Fast N Loud + type: episode + video_codec: H.264 + +? Bake.Off.Italia.S04.1080p.DPLY.WEBRip.AAC2.0.x264-Threshold +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: Threshold + screen_size: 1080p + season: 4 + source: Web + streaming_service: DPlay + title: Bake Off Italia + type: episode + video_codec: H.264 + +? Long.Riders.S01.DSKI.WEBRip.AAC2.0.x264-HorribleSubs +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: HorribleSubs + season: 1 + source: Web + streaming_service: Daisuki + title: Long Riders + type: episode + video_codec: H.264 + +? Milo.Murphys.Law.S01.720p.DSNY.WEB-DL.AAC2.0.x264-TVSmash +: audio_channels: '2.0' + audio_codec: AAC + release_group: TVSmash + screen_size: 720p + season: 1 + source: Web + streaming_service: Disney + title: Milo Murphys Law + type: episode + video_codec: H.264 + +? 30.for.30.S03E15.Doc.and.Darryl.720p.ESPN.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + episode: 15 + episode_title: Doc and Darryl + other: Rip + release_group: BTW + screen_size: 720p + season: 3 + source: Web + streaming_service: ESPN + title: 30 for 30 + type: episode + video_codec: H.264 + +? Boundless.S03.720p.ESQ.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + screen_size: 720p + season: 3 + source: Web + streaming_service: Esquire + title: Boundless + type: episode + video_codec: H.264 + +? Periodismo.Para.Todos.S2016E01.720p.ETTV.WEBRip.AAC2.0.H.264-braggart74 +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: braggart74 + screen_size: 720p + season: 2016 + source: Web + streaming_service: El Trece + title: Periodismo Para Todos + type: episode + video_codec: H.264 + year: 2016 + +? Just.Jillian.S01E01.1080p.ETV.WEBRip.AAC2.0.x264-GoApe +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: GoApe + screen_size: 1080p + season: 1 + source: Web + streaming_service: E! + title: Just Jillian + type: episode + video_codec: H.264 + +? New.Money.S01.1080p.ETV.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 1080p + season: 1 + source: Web + streaming_service: E! + title: New Money + type: episode + video_codec: H.264 + +? Gaming.Show.In.My.Parents.Garage.S02E01.The.Power.Up1000.FAM.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + episode_title: The Power Up1000 + other: Rip + release_group: RTN + season: 2 + source: Web + streaming_service: Family + title: Gaming Show In My Parents Garage + type: episode + video_codec: H.264 + +? Little.People.2016.S01E03.Proud.to.Be.You.and.Me.720p.FJR.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 3 + episode_title: Proud to Be You and Me + other: Rip + release_group: RTN + screen_size: 720p + season: 1 + source: Web + streaming_service: Family Jr + title: Little People + type: episode + video_codec: H.264 + year: 2016 + +? The.Pioneer.Woman.S00E08.Summer.Summer.Summer.720p.FOOD.WEB-DL.AAC2.0.x264-AJP69 +: audio_channels: '2.0' + audio_codec: AAC + episode: 8 + episode_title: Summer Summer Summer + release_group: AJP69 + screen_size: 720p + season: 0 + source: Web + streaming_service: Food Network + title: The Pioneer Woman + type: episode + video_codec: H.264 + +? Prata.da.Casa.S01E01.720p.FOX.WEBRip.AAC2.0.H.264-BARRY +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: BARRY + screen_size: 720p + season: 1 + source: Web + streaming_service: Fox + title: Prata da Casa + type: episode + video_codec: H.264 + +? Grandfathered.S01.720p.FOX.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + season: 1 + source: Web + streaming_service: Fox + title: Grandfathered + type: episode + video_codec: H.264 + +? Truth.and.Iliza.S01E01.FREE.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: BTN + season: 1 + source: Web + streaming_service: Freeform + title: Truth and Iliza + type: episode + video_codec: H.264 + +? Seven.Year.Switch.S01.720p.FYI.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + season: 1 + source: Web + streaming_service: FYI Network + title: Seven Year Switch + type: episode + video_codec: H.264 + +? NHL.2015.10.09.Leafs.vs.Red.Wings.Condensed.Game.720p.Away.Feed.GC.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + date: 2015-10-09 + episode_title: Leafs vs Red Wings Condensed Game + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: NHL GameCenter + title: NHL + type: episode + video_codec: H.264 + +? NHL.2016.01.26.Maple.Leafs.vs.Panthers.720p.Home.Feed.GC.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + date: 2016-01-26 + episode_title: Maple Leafs vs Panthers + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: NHL GameCenter + title: NHL + type: episode + video_codec: H.264 + +? Big.Brother.Canada.S05.GLBL.WEBRip.AAC2.0.H.264-RTN +: audio_channels: '2.0' + audio_codec: AAC + country: CA + other: Rip + release_group: RTN + season: 5 + source: Web + streaming_service: Global + title: Big Brother + type: episode + video_codec: H.264 + +? Pornolandia.S01.720p.GLOB.WEBRip.AAC2.0.x264-GeneX +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: GeneX + screen_size: 720p + season: 1 + source: Web + streaming_service: GloboSat Play + title: Pornolandia + type: episode + video_codec: H.264 + +? Transando.com.Laerte.S01.720p.GLOB.WEBRip.AAC2.0.x264-GeneX +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: GeneX + screen_size: 720p + season: 1 + source: Web + streaming_service: GloboSat Play + title: Transando com Laerte + type: episode + video_codec: H.264 + +? Flip.or.Flop.S01.720p.HGTV.WEBRip.AAC2.0.H.264-AJP69 +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: AJP69 + screen_size: 720p + season: 1 + source: Web + streaming_service: HGTV + title: Flip or Flop + type: episode + video_codec: H.264 + +? Kitten.Bowl.2014.720p.HLMK.WEBRip.AAC2.0.x264-monkee +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: monkee + screen_size: 720p + source: Web + streaming_service: Hallmark + title: Kitten Bowl + type: movie + video_codec: H.264 + year: 2014 + +? Still.Star-Crossed.S01E05.720p.HULU.WEB-DL.AAC2.0.H.264-VLAD +: audio_channels: '2.0' + audio_codec: AAC + episode: 5 + release_group: VLAD + screen_size: 720p + season: 1 + source: Web + streaming_service: Hulu + title: Still Star-Crossed + type: episode + video_codec: H.264 + +? EastEnders.2017.07.17.720p.iP.WEB-DL.AAC2.0.H.264-BTN +: audio_channels: '2.0' + audio_codec: AAC + date: 2017-07-17 + release_group: BTN + screen_size: 720p + source: Web + streaming_service: BBC iPlayer + title: EastEnders + type: episode + video_codec: H.264 + +? Handmade.in.Japan.S01E01.720p.iP.WEBRip.AAC2.0.H.264-SUP +: audio_channels: '2.0' + audio_codec: AAC + country: JP + episode: 1 + other: Rip + release_group: SUP + screen_size: 720p + season: 1 + source: Web + streaming_service: BBC iPlayer + title: Handmade in + type: episode + video_codec: H.264 + +? The.Chillenden.Murders.S01.720p.iP.WEBRip.AAC2.0.H.264-HAX +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: HAX + screen_size: 720p + season: 1 + source: Web + streaming_service: BBC iPlayer + title: The Chillenden Murders + type: episode + video_codec: H.264 + +? The.Street.S01.ITV.WEB-DL.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + release_group: RTN + season: 1 + source: Web + streaming_service: ITV + title: The Street + type: episode + video_codec: H.264 + +? Hope.for.Wildlife.S04.1080p.KNOW.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 1080p + season: 4 + source: Web + streaming_service: Knowledge Network + title: Hope for Wildlife + type: episode + video_codec: H.264 + +? Kim.of.Queens.S02.720p.LIFE.WEBRip.AAC2.0.H.264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + screen_size: 720p + season: 2 + source: Web + streaming_service: Lifetime + title: Kim of Queens + type: episode + video_codec: H.264 + +? The.Rachel.Maddow.Show.2017.02.22.720p.MNBC.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + date: 2017-02-22 + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: MSNBC + title: The Rachel Maddow Show + type: episode + video_codec: H.264 + +? Ignition.S06E12.720p.MTOD.WEB-DL.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 12 + release_group: RTN + screen_size: 720p + season: 6 + source: Web + streaming_service: Motor Trend OnDemand + title: Ignition + type: episode + video_codec: H.264 + +? Teen.Mom.UK.S01E01.Life.as.a.Teen.Mum.1080p.MTV.WEB-DL.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + country: GB + episode: 1 + episode_title: Life as a Teen Mum + release_group: BTW + screen_size: 1080p + season: 1 + source: Web + streaming_service: MTV + title: Teen Mom + type: episode + video_codec: H.264 + +? Undrafted.S01.720p.NFLN.WEBRip.AAC2.0.H.264-TTYL +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: TTYL + screen_size: 720p + season: 1 + source: Web + streaming_service: NFL Now + title: Undrafted + type: episode + video_codec: H.264 + +? NFL.2016.08.25.PreSeason.Cowboys.vs.Seahawks.720p.NFL.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + date: 2016-08-25 + episode_title: PreSeason Cowboys vs Seahawks + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: NFL + title: NFL + type: episode + video_codec: H.264 + +? Bunsen.is.a.Beast.S01E23.Guinea.Some.Lovin.1080p.NICK.WEBRip.AAC2.0.x264-TVSmash +: audio_channels: '2.0' + audio_codec: AAC + country: GN + episode: 23 + episode_title: Some Lovin + other: Rip + release_group: TVSmash + screen_size: 1080p + season: 1 + source: Web + streaming_service: Nickelodeon + title: Bunsen is a Beast + type: episode + video_codec: H.264 + +? Valkyrie.S01.720p.NRK.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + screen_size: 720p + season: 1 + source: Web + streaming_service: Norsk Rikskringkasting + title: Valkyrie + type: episode + video_codec: H.264 + +? Food.Forward.S01.720p.PBS.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + screen_size: 720p + season: 1 + source: Web + streaming_service: PBS + title: Food Forward + type: episode + video_codec: H.264 + +? SciGirls.S01E01.Turtle.Mania.720p.PBSK.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + episode_title: Turtle Mania + other: Rip + release_group: RTN + screen_size: 720p + season: 1 + source: Web + streaming_service: PBS Kids + title: SciGirls + type: episode + video_codec: H.264 + +? Powers.2015.S01.1080p.PSN.WEBRip.DD5.1.x264-NTb +: audio_channels: '5.1' + audio_codec: Dolby Digital + other: Rip + release_group: NTb + screen_size: 1080p + season: 1 + source: Web + streaming_service: Playstation Network + title: Powers + type: episode + video_codec: H.264 + year: 2015 + +? Escape.The.Night.S02E02.The.Masquerade.Part.II.1080p.RED.WEBRip.AAC5.1.VP9-BTW +: audio_channels: '5.1' + audio_codec: AAC + episode: 2 + episode_title: The Masquerade + other: Rip + part: 2 + release_group: VP9-BTW + screen_size: 1080p + season: 2 + source: Web + streaming_service: YouTube Red + title: Escape The Night + type: episode + +? Escape.The.Night.S02E02.The.Masquerade.Part.II.2160p.RED.WEBRip.AAC5.1.VP9-BTW +: audio_channels: '5.1' + audio_codec: AAC + episode: 2 + episode_title: The Masquerade + other: Rip + part: 2 + release_group: VP9-BTW + screen_size: 2160p + season: 2 + source: Web + streaming_service: YouTube Red + title: Escape The Night + type: episode + +? Escape.The.Night.S02E02.The.Masquerade.Part.II.720p.RED.WEBRip.AAC5.1.VP9-BTW +: audio_channels: '5.1' + audio_codec: AAC + episode: 2 + episode_title: The Masquerade + other: Rip + part: 2 + release_group: VP9-BTW + screen_size: 720p + season: 2 + source: Web + streaming_service: YouTube Red + title: Escape The Night + type: episode + +? The.Family.Law.S02E01.720p.SBS.WEB-DL.AAC2.0.H.264-BTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + release_group: BTN + screen_size: 720p + season: 2 + source: Web + streaming_service: SBS (AU) + title: The Family Law + type: episode + video_codec: H.264 + +? Theres.No.Joy.In.Beachville.The.True.Story.of.Baseballs.Origin.720p.SNET.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + source: Web + streaming_service: Sportsnet + title: Theres No Joy In Beachville The True Story of Baseballs Origin + type: movie + video_codec: H.264 + +? One.Night.Only.Alec.Baldwin.720p.SPIK.WEB-DL.AAC2.0.x264-NOGRP +: audio_channels: '2.0' + audio_codec: AAC + release_group: NOGRP + screen_size: 720p + source: Web + streaming_service: Spike + title: One Night Only Alec Baldwin + type: movie + video_codec: H.264 + +? Ink.Master.S08.720p.SPIK.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 720p + season: 8 + source: Web + streaming_service: Spike + title: Ink Master + type: episode + video_codec: H.264 + +? Jungle.Bunch.S01E01.Deep.Chasm.1080p.SPRT.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + episode_title: Deep Chasm + other: Rip + release_group: RTN + screen_size: 1080p + season: 1 + source: Web + streaming_service: Sprout + title: Jungle Bunch + type: episode + video_codec: H.264 + +? Ash.vs.Evil.Dead.S01.720p.STZ.WEBRip.AAC2.0.x264-NTb +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: NTb + screen_size: 720p + season: 1 + source: Web + streaming_service: Starz + title: Ash vs Evil Dead + type: episode + video_codec: H.264 + +? WWE.Swerved.S01.720p.WWEN.WEBRip.AAC2.0.H.264-PPKORE +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: PPKORE + screen_size: 720p + season: 1 + source: Web + streaming_service: WWE Network + title: WWE Swerved + type: episode + video_codec: H.264 + +? Face.Off.S11.1080p.SYFY.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + screen_size: 1080p + season: 11 + source: Web + streaming_service: Syfy + title: Face Off + type: episode + video_codec: H.264 + +? Conan.2016.09.22.Jeff.Garlin.720p.TBS.WEBRip.AAC2.0.H.264-NOGRP +: audio_channels: '2.0' + audio_codec: AAC + date: 2016-09-22 + episode_title: Jeff Garlin + other: Rip + release_group: NOGRP + screen_size: 720p + source: Web + streaming_service: TBS + title: Conan + type: episode + video_codec: H.264 + +? Swans.Crossing.S01.TUBI.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + season: 1 + source: Web + streaming_service: TubiTV + title: Swans Crossing + type: episode + video_codec: H.264 + +? The.Joy.of.Techs.S01.UKTV.WEB-DL.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + release_group: RTN + season: 1 + source: Web + streaming_service: UKTV + title: The Joy of Techs + type: episode + video_codec: H.264 + +? Rock.Icons.S01.720p.VH1.WEB-DL.AAC2.0.H.264-RTN +: audio_channels: '2.0' + audio_codec: AAC + release_group: RTN + screen_size: 720p + season: 1 + source: Web + streaming_service: VH1 + title: Rock Icons + type: episode + video_codec: H.264 + +? Desus.and.Mero.S01E130.2017.07.18.1080p.VICE.WEB-DL.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + date: 2017-07-18 + episode: 130 + release_group: RTN + screen_size: 1080p + season: 1 + source: Web + streaming_service: Viceland + title: Desus and Mero + type: episode + video_codec: H.264 + +? Graveyard.Carz.S07.1080p.VLCT.WEBRip.AAC2.0.x264-RTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: RTN + screen_size: 1080p + season: 7 + source: Web + streaming_service: Velocity + title: Graveyard Carz + type: episode + video_codec: H.264 + +? Other.Space.S01E01.1080p.YHOO.WEBRip.AAC2.0.x264-BTW +: audio_channels: '2.0' + audio_codec: AAC + episode: 1 + other: Rip + release_group: BTW + screen_size: 1080p + season: 1 + source: Web + streaming_service: Yahoo + title: Other Space + type: episode + video_codec: H.264 + +? Americas.Test.Kitchen.S17.720p.ATK.WEB-DL.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + release_group: BTN + screen_size: 720p + season: 17 + source: Web + streaming_service: America's Test Kitchen + title: Americas Test Kitchen + type: episode + video_codec: H.264 + +? Bushwhacked.Bugs.S01.AUBC.WEBRip.AAC2.0.H.264-DAWN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: DAWN + season: 1 + source: Web + streaming_service: ABC Australia + title: Bushwhacked Bugs + type: episode + video_codec: H.264 + +? VICE.S05E12.1080p.HBO.WEB-DL.AAC2.0.H.264-monkee +? VICE.S05E12.1080p.HBO-Go.WEB-DL.AAC2.0.H.264-monkee +? VICE.S05E12.1080p.HBOGo.WEB-DL.AAC2.0.H.264-monkee +: audio_channels: '2.0' + audio_codec: AAC + episode: 12 + release_group: monkee + screen_size: 1080p + season: 5 + source: Web + streaming_service: HBO Go + title: VICE + type: episode + video_codec: H.264 + +? Dix.Pour.Cent.S02.PLUZ.WEBRip.AAC2.0.H.264-TURTLE +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: TURTLE + season: 2 + source: Web + streaming_service: Pluzz + title: Dix Pour Cent + type: episode + video_codec: H.264 + +? Ulveson.och.Herngren.S01.720p.SVT.WEBRip.AAC2.0.H.264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + screen_size: 720p + season: 1 + source: Web + streaming_service: Sveriges Television + title: Ulveson och Herngren + type: episode + video_codec: H.264 + +? Bravest.Warriors.S03.1080p.VRV.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + screen_size: 1080p + season: 3 + source: Web + streaming_service: VRV + title: Bravest Warriors + type: episode + video_codec: H.264 + +? The.Late.Night.Big.Breakfast.S02.WME.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + season: 2 + source: Web + streaming_service: WatchMe + title: The Late Night Big Breakfast + type: episode + video_codec: H.264 + +? Hockey.Wives.S02.WNET.WEBRip.AAC2.0.H.264-BTW +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTW + season: 2 + source: Web + streaming_service: W Network + title: Hockey Wives + type: episode + video_codec: H.264 + +? Sin.City.Saints.S01.1080p.YHOO.WEBRip.AAC2.0.x264-NTb +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: NTb + screen_size: 1080p + season: 1 + source: Web + streaming_service: Yahoo + title: Sin City Saints + type: episode + video_codec: H.264 + +? 555.S01.1080p.VMEO.WEBRip.AAC2.0.x264-BTN +: audio_channels: '2.0' + audio_codec: AAC + other: Rip + release_group: BTN + screen_size: 1080p + season: 1 + source: Web + streaming_service: Vimeo + title: '555' + type: episode + video_codec: H.264 + +# All this below shouldn't match any streaming services +? London.2012.Olympics.CTV.Preview.Show.HDTV.x264-2HD +: alternative_title: Olympics CTV Preview Show + release_group: 2HD + source: HDTV + title: London + type: movie + video_codec: H.264 + year: 2012 + +? UFC.on.FOX.24.1080p.HDTV.x264-VERUM +: episode: 24 + release_group: VERUM + screen_size: 1080p + source: HDTV + title: UFC on FOX + type: episode + video_codec: H.264 + +? ESPN.E.60.2016.10.04.HDTV.x264-LoTV +: date: 2016-10-04 + episode: 60 + release_group: LoTV + source: HDTV + title: ESPN E + type: episode + video_codec: H.264 + +? GTTV.E3.All.Access.Live.Day.1.Xbox.Showcase.Preshow.HDTV.x264-SYS +: episode: 3 + episode_title: All Access Live Day 1 Xbox Showcase Preshow + release_group: SYS + source: HDTV + title: GTTV + type: episode + video_codec: H.264 diff --git a/libs/guessit/test/test-input-file.txt b/libs/guessit/test/test-input-file.txt new file mode 100644 index 000000000..656bc9317 --- /dev/null +++ b/libs/guessit/test/test-input-file.txt @@ -0,0 +1,2 @@ +Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv +SecondFile.avi
\ No newline at end of file diff --git a/libs/guessit/test/test_api.py b/libs/guessit/test/test_api.py new file mode 100644 index 000000000..9abb84d9f --- /dev/null +++ b/libs/guessit/test/test_api.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name, pointless-string-statement + +import os + +import pytest +import six + +from ..api import guessit, properties, GuessitException + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def test_default(): + ret = guessit('Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret + + +def test_forced_unicode(): + ret = guessit(u'Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret and isinstance(ret['title'], six.text_type) + + +def test_forced_binary(): + ret = guessit(b'Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret and isinstance(ret['title'], six.binary_type) + + [email protected]('sys.version_info < (3, 4)', reason="Path is not available") +def test_pathlike_object(): + from pathlib import Path + path = Path('Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + ret = guessit(path) + assert ret and 'title' in ret + + +def test_unicode_japanese(): + ret = guessit('[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi') + assert ret and 'title' in ret + + +def test_unicode_japanese_options(): + ret = guessit("[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": ["阿维达"]}) + assert ret and 'title' in ret and ret['title'] == "阿维达" + + +def test_forced_unicode_japanese_options(): + ret = guessit(u"[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": [u"阿维达"]}) + assert ret and 'title' in ret and ret['title'] == u"阿维达" + +# TODO: This doesn't compile on python 3, but should be tested on python 2. +""" +if six.PY2: + def test_forced_binary_japanese_options(): + ret = guessit(b"[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": [b"阿维达"]}) + assert ret and 'title' in ret and ret['title'] == b"阿维达" +""" + + +def test_properties(): + props = properties() + assert 'video_codec' in props.keys() + + +def test_exception(): + with pytest.raises(GuessitException) as excinfo: + guessit(object()) + assert "An internal error has occured in guessit" in str(excinfo.value) + assert "Guessit Exception Report" in str(excinfo.value) + assert "Please report at https://github.com/guessit-io/guessit/issues" in str(excinfo.value) diff --git a/libs/guessit/test/test_api_unicode_literals.py b/libs/guessit/test/test_api_unicode_literals.py new file mode 100644 index 000000000..826f7cd16 --- /dev/null +++ b/libs/guessit/test/test_api_unicode_literals.py @@ -0,0 +1,74 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name, pointless-string-statement + + +from __future__ import unicode_literals + +import os + +import pytest +import six + +from ..api import guessit, properties, GuessitException + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def test_default(): + ret = guessit('Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret + + +def test_forced_unicode(): + ret = guessit(u'Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret and isinstance(ret['title'], six.text_type) + + +def test_forced_binary(): + ret = guessit(b'Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + assert ret and 'title' in ret and isinstance(ret['title'], six.binary_type) + + +def test_unicode_japanese(): + ret = guessit('[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi') + assert ret and 'title' in ret + + +def test_unicode_japanese_options(): + ret = guessit("[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": ["阿维达"]}) + assert ret and 'title' in ret and ret['title'] == "阿维达" + + +def test_forced_unicode_japanese_options(): + ret = guessit(u"[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": [u"阿维达"]}) + assert ret and 'title' in ret and ret['title'] == u"阿维达" + +# TODO: This doesn't compile on python 3, but should be tested on python 2. +""" +if six.PY2: + def test_forced_binary_japanese_options(): + ret = guessit(b"[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi", options={"expected_title": [b"阿维达"]}) + assert ret and 'title' in ret and ret['title'] == b"阿维达" +""" + + +def test_ensure_standard_string_class(): + class CustomStr(str): + pass + + ret = guessit(CustomStr('1080p'), options={'advanced': True}) + assert ret and 'screen_size' in ret and not isinstance(ret['screen_size'].input_string, CustomStr) + + +def test_properties(): + props = properties() + assert 'video_codec' in props.keys() + + +def test_exception(): + with pytest.raises(GuessitException) as excinfo: + guessit(object()) + assert "An internal error has occured in guessit" in str(excinfo.value) + assert "Guessit Exception Report" in str(excinfo.value) + assert "Please report at https://github.com/guessit-io/guessit/issues" in str(excinfo.value) diff --git a/libs/guessit/test/test_benchmark.py b/libs/guessit/test/test_benchmark.py new file mode 100644 index 000000000..34386e307 --- /dev/null +++ b/libs/guessit/test/test_benchmark.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use,pointless-statement,missing-docstring,invalid-name,line-too-long +import time + +import pytest + +from ..api import guessit + + +def case1(): + return guessit('Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv') + + +def case2(): + return guessit('Movies/Fantastic Mr Fox/Fantastic.Mr.Fox.2009.DVDRip.{x264+LC-AAC.5.1}{Fr-Eng}{Sub.Fr-Eng}-™.[sharethefiles.com].mkv') + + +def case3(): + return guessit('Series/dexter/Dexter.5x02.Hello,.Bandit.ENG.-.sub.FR.HDTV.XviD-AlFleNi-TeaM.[tvu.org.ru].avi') + + +def case4(): + return guessit('Movies/The Doors (1991)/09.03.08.The.Doors.(1991).BDRip.720p.AC3.X264-HiS@SiLUHD-English.[sharethefiles.com].mkv') + + + group="Performance Tests", + min_time=1, + max_time=2, + min_rounds=5, + timer=time.time, + disable_gc=True, + warmup=False +) [email protected](True, reason="Disabled") +class TestBenchmark(object): + def test_case1(self, benchmark): + ret = benchmark(case1) + assert ret + + def test_case2(self, benchmark): + ret = benchmark(case2) + assert ret + + def test_case3(self, benchmark): + ret = benchmark(case3) + assert ret + + def test_case4(self, benchmark): + ret = benchmark(case4) + assert ret diff --git a/libs/guessit/test/test_main.py b/libs/guessit/test/test_main.py new file mode 100644 index 000000000..cbdba7aa4 --- /dev/null +++ b/libs/guessit/test/test_main.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name + +import os + +import pytest + +from ..__main__ import main + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def test_main_no_args(): + main([]) + + +def test_main(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv']) + + +def test_main_unicode(): + main(['[阿维达].Avida.2006.FRENCH.DVDRiP.XViD-PROD.avi']) + + +def test_main_forced_unicode(): + main([u'Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv']) + + +def test_main_verbose(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv', '--verbose']) + + +def test_main_yaml(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv', '--yaml']) + + +def test_main_json(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv', '--json']) + + +def test_main_show_property(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv', '-P', 'title']) + + +def test_main_advanced(): + main(['Fear.and.Loathing.in.Las.Vegas.FRENCH.ENGLISH.720p.HDDVD.DTS.x264-ESiR.mkv', '-a']) + + +def test_main_input(): + main(['--input', os.path.join(__location__, 'test-input-file.txt')]) + + +def test_main_properties(): + main(['-p']) + main(['-p', '--json']) + main(['-p', '--yaml']) + + +def test_main_values(): + main(['-V']) + main(['-V', '--json']) + main(['-V', '--yaml']) + + +def test_main_help(): + with pytest.raises(SystemExit): + main(['--help']) + + +def test_main_version(): + main(['--version']) diff --git a/libs/guessit/test/test_options.py b/libs/guessit/test/test_options.py new file mode 100644 index 000000000..837497855 --- /dev/null +++ b/libs/guessit/test/test_options.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name, pointless-string-statement +import os + +import pytest + +from ..options import get_config_file_locations, merge_configurations, load_config_file, ConfigurationException, \ + load_config + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + + +def test_config_locations(): + homedir = '/root' + cwd = '/root/cwd' + + locations = get_config_file_locations(homedir, cwd, True) + assert len(locations) == 9 + + assert '/root/.guessit/options.json' in locations + assert '/root/.guessit/options.yml' in locations + assert '/root/.guessit/options.yaml' in locations + assert '/root/.config/guessit/options.json' in locations + assert '/root/.config/guessit/options.yml' in locations + assert '/root/.config/guessit/options.yaml' in locations + assert '/root/cwd/guessit.options.json' in locations + assert '/root/cwd/guessit.options.yml' in locations + assert '/root/cwd/guessit.options.yaml' in locations + + +def test_merge_configurations(): + c1 = {'param1': True, 'param2': True, 'param3': False} + c2 = {'param1': False, 'param2': True, 'param3': False} + c3 = {'param1': False, 'param2': True, 'param3': False} + + merged = merge_configurations(c1, c2, c3) + assert not merged['param1'] + assert merged['param2'] + assert not merged['param3'] + + merged = merge_configurations(c3, c2, c1) + assert merged['param1'] + assert merged['param2'] + assert not merged['param3'] + + +def test_merge_configurations_lists(): + c1 = {'param1': [1], 'param2': True, 'param3': False} + c2 = {'param1': [2], 'param2': True, 'param3': False} + c3 = {'param1': [3], 'param2': True, 'param3': False} + + merged = merge_configurations(c1, c2, c3) + assert merged['param1'] == [1, 2, 3] + assert merged['param2'] + assert not merged['param3'] + + merged = merge_configurations(c3, c2, c1) + assert merged['param1'] == [3, 2, 1] + assert merged['param2'] + assert not merged['param3'] + + +def test_merge_configurations_pristine_all(): + c1 = {'param1': [1], 'param2': True, 'param3': False} + c2 = {'param1': [2], 'param2': True, 'param3': False, 'pristine': True} + c3 = {'param1': [3], 'param2': True, 'param3': False} + + merged = merge_configurations(c1, c2, c3) + assert merged['param1'] == [2, 3] + assert merged['param2'] + assert not merged['param3'] + + merged = merge_configurations(c3, c2, c1) + assert merged['param1'] == [2, 1] + assert merged['param2'] + assert not merged['param3'] + + +def test_merge_configurations_pristine_properties(): + c1 = {'param1': [1], 'param2': False, 'param3': True} + c2 = {'param1': [2], 'param2': True, 'param3': False, 'pristine': ['param2', 'param3']} + c3 = {'param1': [3], 'param2': True, 'param3': False} + + merged = merge_configurations(c1, c2, c3) + assert merged['param1'] == [1, 2, 3] + assert merged['param2'] + assert not merged['param3'] + + +def test_merge_configurations_pristine_properties2(): + c1 = {'param1': [1], 'param2': False, 'param3': True} + c2 = {'param1': [2], 'param2': True, 'param3': False, 'pristine': ['param1', 'param2', 'param3']} + c3 = {'param1': [3], 'param2': True, 'param3': False} + + merged = merge_configurations(c1, c2, c3) + assert merged['param1'] == [2, 3] + assert merged['param2'] + assert not merged['param3'] + + +def test_load_config_file(): + json_config = load_config_file(os.path.join(__location__, 'config', 'test.json')) + yml_config = load_config_file(os.path.join(__location__, 'config', 'test.yml')) + yaml_config = load_config_file(os.path.join(__location__, 'config', 'test.yaml')) + + assert json_config['expected_title'] == ['The 100', 'OSS 117'] + assert yml_config['expected_title'] == ['The 100', 'OSS 117'] + assert yaml_config['expected_title'] == ['The 100', 'OSS 117'] + + assert json_config['yaml'] is False + assert yml_config['yaml'] is True + assert yaml_config['yaml'] is True + + with pytest.raises(ConfigurationException) as excinfo: + load_config_file(os.path.join(__location__, 'config', 'dummy.txt')) + + assert excinfo.match('Configuration file extension is not supported for ".*?dummy.txt" file\\.') + + +def test_load_config(): + config = load_config({'no_embedded_config': True, 'param1': 'test', + 'config': [os.path.join(__location__, 'config', 'test.yml')]}) + + assert config['param1'] == 'test' + + assert config['expected_title'] == ['The 100', 'OSS 117'] + assert config['yaml'] is True + + config = load_config({'no_embedded_config': True, 'param1': 'test'}) + + assert config['param1'] == 'test' + + assert 'expected_title' not in config + assert 'yaml' not in config + + config = load_config({'no_embedded_config': True, 'param1': 'test', 'config': ['false']}) + + assert config['param1'] == 'test' + + assert 'expected_title' not in config + assert 'yaml' not in config diff --git a/libs/guessit/test/test_yml.py b/libs/guessit/test/test_yml.py new file mode 100644 index 000000000..c86609392 --- /dev/null +++ b/libs/guessit/test/test_yml.py @@ -0,0 +1,288 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# pylint: disable=no-self-use, pointless-statement, missing-docstring, invalid-name +import logging + +# io.open supports encoding= in python 2.7 +from io import open # pylint: disable=redefined-builtin +import os +import yaml + +import six + +import babelfish +import pytest + +from rebulk.remodule import re +from rebulk.utils import is_iterable + +from ..options import parse_options, load_config +from ..yamlutils import OrderedDictYAMLLoader +from .. import guessit + + +logger = logging.getLogger(__name__) + +__location__ = os.path.realpath(os.path.join(os.getcwd(), os.path.dirname(__file__))) + +filename_predicate = None +string_predicate = None + + +# filename_predicate = lambda filename: 'episode_title' in filename +# string_predicate = lambda string: '-DVD.BlablaBla.Fix.Blablabla.XVID' in string + + +class EntryResult(object): + def __init__(self, string, negates=False): + self.string = string + self.negates = negates + self.valid = [] + self.missing = [] + self.different = [] + self.extra = [] + self.others = [] + + @property + def ok(self): + if self.negates: + return self.missing or self.different + return not self.missing and not self.different and not self.extra and not self.others + + @property + def warning(self): + if self.negates: + return False + return not self.missing and not self.different and self.extra + + @property + def error(self): + if self.negates: + return not self.missing and not self.different and not self.others + return self.missing or self.different or self.others + + def __repr__(self): + if self.ok: + return self.string + ': OK!' + elif self.warning: + return '%s%s: WARNING! (valid=%i, extra=%i)' % ('-' if self.negates else '', self.string, len(self.valid), + len(self.extra)) + elif self.error: + return '%s%s: ERROR! (valid=%i, missing=%i, different=%i, extra=%i, others=%i)' % \ + ('-' if self.negates else '', self.string, len(self.valid), len(self.missing), len(self.different), + len(self.extra), len(self.others)) + + return '%s%s: UNKOWN! (valid=%i, missing=%i, different=%i, extra=%i, others=%i)' % \ + ('-' if self.negates else '', self.string, len(self.valid), len(self.missing), len(self.different), + len(self.extra), len(self.others)) + + @property + def details(self): + ret = [] + if self.valid: + ret.append('valid=' + str(len(self.valid))) + for valid in self.valid: + ret.append(' ' * 4 + str(valid)) + if self.missing: + ret.append('missing=' + str(len(self.missing))) + for missing in self.missing: + ret.append(' ' * 4 + str(missing)) + if self.different: + ret.append('different=' + str(len(self.different))) + for different in self.different: + ret.append(' ' * 4 + str(different)) + if self.extra: + ret.append('extra=' + str(len(self.extra))) + for extra in self.extra: + ret.append(' ' * 4 + str(extra)) + if self.others: + ret.append('others=' + str(len(self.others))) + for other in self.others: + ret.append(' ' * 4 + str(other)) + return ret + + +class Results(list): + def assert_ok(self): + errors = [entry for entry in self if entry.error] + assert not errors + + +def files_and_ids(predicate=None): + files = [] + ids = [] + + for (dirpath, _, filenames) in os.walk(__location__): + if os.path.split(dirpath)[-1] == 'config': + continue + if dirpath == __location__: + dirpath_rel = '' + else: + dirpath_rel = os.path.relpath(dirpath, __location__) + for filename in filenames: + name, ext = os.path.splitext(filename) + filepath = os.path.join(dirpath_rel, filename) + if ext == '.yml' and (not predicate or predicate(filepath)): + files.append(filepath) + ids.append(os.path.join(dirpath_rel, name)) + + return files, ids + + +class TestYml(object): + """ + Run tests from yaml files. + Multiple input strings having same expected results can be chained. + Use $ marker to check inputs that should not match results. + """ + + options_re = re.compile(r'^([ +-]+)(.*)') + + files, ids = files_and_ids(filename_predicate) + + @staticmethod + def set_default(expected, default): + if default: + for k, v in default.items(): + if k not in expected: + expected[k] = v + + @pytest.mark.parametrize('filename', files, ids=ids) + def test(self, filename, caplog): + caplog.set_level(logging.INFO) + with open(os.path.join(__location__, filename), 'r', encoding='utf-8') as infile: + data = yaml.load(infile, OrderedDictYAMLLoader) + entries = Results() + + last_expected = None + for string, expected in reversed(list(data.items())): + if expected is None: + data[string] = last_expected + else: + last_expected = expected + + default = None + try: + default = data['__default__'] + del data['__default__'] + except KeyError: + pass + + for string, expected in data.items(): + TestYml.set_default(expected, default) + entry = self.check_data(filename, string, expected) + entries.append(entry) + entries.assert_ok() + + def check_data(self, filename, string, expected): + if six.PY2: + if isinstance(string, six.text_type): + string = string.encode('utf-8') + converts = [] + for k, v in expected.items(): + if isinstance(v, six.text_type): + v = v.encode('utf-8') + converts.append((k, v)) + for k, v in converts: + expected[k] = v + if not isinstance(string, str): + string = str(string) + if not string_predicate or string_predicate(string): # pylint: disable=not-callable + entry = self.check(string, expected) + if entry.ok: + logger.debug('[' + filename + '] ' + str(entry)) + elif entry.warning: + logger.warning('[' + filename + '] ' + str(entry)) + elif entry.error: + logger.error('[' + filename + '] ' + str(entry)) + for line in entry.details: + logger.error('[' + filename + '] ' + ' ' * 4 + line) + return entry + + def check(self, string, expected): + negates, global_, string = self.parse_token_options(string) + + options = expected.get('options') + if options is None: + options = {} + if not isinstance(options, dict): + options = parse_options(options) + options['config'] = False + options = load_config(options) + try: + result = guessit(string, options) + except Exception as exc: + logger.error('[' + string + '] Exception: ' + str(exc)) + raise exc + + entry = EntryResult(string, negates) + + if global_: + self.check_global(string, result, entry) + + self.check_expected(result, expected, entry) + + return entry + + def parse_token_options(self, string): + matches = self.options_re.search(string) + negates = False + global_ = False + if matches: + string = matches.group(2) + for opt in matches.group(1): + if '-' in opt: + negates = True + if '+' in opt: + global_ = True + return negates, global_, string + + def check_global(self, string, result, entry): + global_span = [] + for result_matches in result.matches.values(): + for result_match in result_matches: + if not global_span: + global_span = list(result_match.span) + else: + if global_span[0] > result_match.span[0]: + global_span[0] = result_match.span[0] + if global_span[1] < result_match.span[1]: + global_span[1] = result_match.span[1] + if global_span and global_span[1] - global_span[0] < len(string): + entry.others.append("Match is not global") + + def is_same(self, value, expected): + values = set(value) if is_iterable(value) else set((value,)) + expecteds = set(expected) if is_iterable(expected) else set((expected,)) + if len(values) != len(expecteds): + return False + if isinstance(next(iter(values)), babelfish.Language): + # pylint: disable=no-member + expecteds = set([babelfish.Language.fromguessit(expected) for expected in expecteds]) + elif isinstance(next(iter(values)), babelfish.Country): + # pylint: disable=no-member + expecteds = set([babelfish.Country.fromguessit(expected) for expected in expecteds]) + return values == expecteds + + def check_expected(self, result, expected, entry): + if expected: + for expected_key, expected_value in expected.items(): + if expected_key and expected_key != 'options' and expected_value is not None: + negates_key, _, result_key = self.parse_token_options(expected_key) + if result_key in result.keys(): + if not self.is_same(result[result_key], expected_value): + if negates_key: + entry.valid.append((expected_key, expected_value)) + else: + entry.different.append((expected_key, expected_value, result[result_key])) + else: + if negates_key: + entry.different.append((expected_key, expected_value, result[result_key])) + else: + entry.valid.append((expected_key, expected_value)) + elif not negates_key: + entry.missing.append((expected_key, expected_value)) + + for result_key, result_value in result.items(): + if result_key not in expected.keys(): + entry.extra.append((result_key, result_value)) diff --git a/libs/guessit/test/various.yml b/libs/guessit/test/various.yml new file mode 100644 index 000000000..c95b8e6b3 --- /dev/null +++ b/libs/guessit/test/various.yml @@ -0,0 +1,944 @@ +? Movies/Fear and Loathing in Las Vegas (1998)/Fear.and.Loathing.in.Las.Vegas.720p.HDDVD.DTS.x264-ESiR.mkv +: type: movie + title: Fear and Loathing in Las Vegas + year: 1998 + screen_size: 720p + source: HD-DVD + audio_codec: DTS + video_codec: H.264 + release_group: ESiR + +? Series/Duckman/Duckman - 101 (01) - 20021107 - I, Duckman.avi +: type: episode + title: Duckman + season: 1 + episode: 1 + episode_title: I, Duckman + date: 2002-11-07 + +? Series/Neverwhere/Neverwhere.05.Down.Street.[tvu.org.ru].avi +: type: episode + title: Neverwhere + episode: 5 + episode_title: Down Street + website: tvu.org.ru + +? Neverwhere.05.Down.Street.[tvu.org.ru].avi +: type: episode + title: Neverwhere + episode: 5 + episode_title: Down Street + website: tvu.org.ru + +? Series/Breaking Bad/Minisodes/Breaking.Bad.(Minisodes).01.Good.Cop.Bad.Cop.WEBRip.XviD.avi +: type: episode + title: Breaking Bad + episode_format: Minisode + episode: 1 + episode_title: Good Cop Bad Cop + source: Web + other: Rip + video_codec: Xvid + +? Series/Kaamelott/Kaamelott - Livre V - Ep 23 - Le Forfait.avi +: type: episode + title: Kaamelott + episode: 23 + episode_title: Le Forfait + +? Movies/The Doors (1991)/09.03.08.The.Doors.(1991).BDRip.720p.AC3.X264-HiS@SiLUHD-English.[sharethefiles.com].mkv +: type: movie + title: The Doors + year: 1991 + date: 2008-03-09 + source: Blu-ray + screen_size: 720p + audio_codec: Dolby Digital + video_codec: H.264 + release_group: HiS@SiLUHD + language: english + website: sharethefiles.com + +? Movies/M.A.S.H. (1970)/MASH.(1970).[Divx.5.02][Dual-Subtitulos][DVDRip].ogm +: type: movie + title: MASH + year: 1970 + video_codec: DivX + source: DVD + other: [Dual Audio, Rip] + +? the.mentalist.501.hdtv-lol.mp4 +: type: episode + title: the mentalist + season: 5 + episode: 1 + source: HDTV + release_group: lol + +? the.simpsons.2401.hdtv-lol.mp4 +: type: episode + title: the simpsons + season: 24 + episode: 1 + source: HDTV + release_group: lol + +? Homeland.S02E01.HDTV.x264-EVOLVE.mp4 +: type: episode + title: Homeland + season: 2 + episode: 1 + source: HDTV + video_codec: H.264 + release_group: EVOLVE + +? /media/Band_of_Brothers-e01-Currahee.mkv +: type: episode + title: Band of Brothers + episode: 1 + episode_title: Currahee + +? /media/Band_of_Brothers-x02-We_Stand_Alone_Together.mkv +: type: episode + title: Band of Brothers + bonus: 2 + bonus_title: We Stand Alone Together + +? /movies/James_Bond-f21-Casino_Royale-x02-Stunts.mkv +: type: movie + title: Casino Royale + film_title: James Bond + film: 21 + bonus: 2 + bonus_title: Stunts + +? /TV Shows/new.girl.117.hdtv-lol.mp4 +: type: episode + title: new girl + season: 1 + episode: 17 + source: HDTV + release_group: lol + +? The.Office.(US).1x03.Health.Care.HDTV.XviD-LOL.avi +: type: episode + title: The Office + country: US + season: 1 + episode: 3 + episode_title: Health Care + source: HDTV + video_codec: Xvid + release_group: LOL + +? The_Insider-(1999)-x02-60_Minutes_Interview-1996.mp4 +: type: movie + title: The Insider + year: 1999 + bonus: 2 + bonus_title: 60 Minutes Interview-1996 + +? OSS_117--Cairo,_Nest_of_Spies.mkv +: type: movie + title: OSS 117 + alternative_title: Cairo, Nest of Spies + +? Rush.._Beyond_The_Lighted_Stage-x09-Between_Sun_and_Moon-2002_Hartford.mkv +: type: movie + title: Rush Beyond The Lighted Stage + bonus: 9 + bonus_title: Between Sun and Moon + year: 2002 + +? House.Hunters.International.S56E06.720p.hdtv.x264.mp4 +: type: episode + title: House Hunters International + season: 56 + episode: 6 + screen_size: 720p + source: HDTV + video_codec: H.264 + +? White.House.Down.2013.1080p.BluRay.DTS-HD.MA.5.1.x264-PublicHD.mkv +: type: movie + title: White House Down + year: 2013 + screen_size: 1080p + source: Blu-ray + audio_codec: DTS-HD + audio_profile: Master Audio + video_codec: H.264 + release_group: PublicHD + audio_channels: "5.1" + +? White.House.Down.2013.1080p.BluRay.DTSHD.MA.5.1.x264-PublicHD.mkv +: type: movie + title: White House Down + year: 2013 + screen_size: 1080p + source: Blu-ray + audio_codec: DTS-HD + audio_profile: Master Audio + video_codec: H.264 + release_group: PublicHD + audio_channels: "5.1" + +? Hostages.S01E01.Pilot.for.Air.720p.WEB-DL.DD5.1.H.264-NTb.nfo +: type: episode + title: Hostages + episode_title: Pilot for Air + season: 1 + episode: 1 + screen_size: 720p + source: Web + audio_channels: "5.1" + video_codec: H.264 + audio_codec: Dolby Digital + release_group: NTb + +? Despicable.Me.2.2013.1080p.BluRay.x264-VeDeTT.nfo +: type: movie + title: Despicable Me 2 + year: 2013 + screen_size: 1080p + source: Blu-ray + video_codec: H.264 + release_group: VeDeTT + +? Le Cinquieme Commando 1971 SUBFORCED FRENCH DVDRiP XViD AC3 Bandix.mkv +: type: movie + audio_codec: Dolby Digital + source: DVD + other: Rip + release_group: Bandix + subtitle_language: French + title: Le Cinquieme Commando + video_codec: Xvid + year: 1971 + +? Le Seigneur des Anneaux - La Communauté de l'Anneau - Version Longue - BDRip.mkv +: type: movie + title: Le Seigneur des Anneaux + source: Blu-ray + other: Rip + +? La petite bande (Michel Deville - 1983) VF PAL MP4 x264 AAC.mkv +: type: movie + audio_codec: AAC + language: French + title: La petite bande + video_codec: H.264 + year: 1983 + other: PAL + +? Retour de Flammes (Gregor Schnitzler 2003) FULL DVD.iso +: type: movie + source: DVD + title: Retour de Flammes + type: movie + year: 2003 + +? A.Common.Title.Special.2014.avi +: type: movie + year: 2014 + title: A Common Title Special + +? A.Common.Title.2014.Special.avi +: type: episode + year: 2014 + title: A Common Title + episode_title: Special + episode_details: Special + +? A.Common.Title.2014.Special.Edition.avi +: type: movie + year: 2014 + title: A Common Title + edition: Special + +? Downton.Abbey.2013.Christmas.Special.HDTV.x264-FoV.mp4 +: type: episode + year: 2013 + title: Downton Abbey + episode_title: Christmas Special + video_codec: H.264 + release_group: FoV + source: HDTV + episode_details: Special + +? Doctor_Who_2013_Christmas_Special.The_Time_of_The_Doctor.HD +: type: episode + title: Doctor Who + other: HD + episode_details: Special + episode_title: Christmas Special The Time of The Doctor + year: 2013 + +? Doctor Who 2005 50th Anniversary Special The Day of the Doctor 3.avi +: type: episode + title: Doctor Who + episode_details: Special + episode_title: 50th Anniversary Special The Day of the Doctor 3 + year: 2005 + +? Robot Chicken S06-Born Again Virgin Christmas Special HDTV x264.avi +: type: episode + title: Robot Chicken + source: HDTV + season: 6 + episode_title: Born Again Virgin Christmas Special + video_codec: H.264 + episode_details: Special + +? Wicked.Tuna.S03E00.Head.To.Tail.Special.HDTV.x264-YesTV +: type: episode + title: Wicked Tuna + episode_title: Head To Tail Special + release_group: YesTV + season: 3 + episode: 0 + video_codec: H.264 + source: HDTV + episode_details: Special + +? The.Voice.UK.S03E12.HDTV.x264-C4TV +: episode: 12 + video_codec: H.264 + source: HDTV + title: The Voice + release_group: C4TV + season: 3 + country: United Kingdom + type: episode + +? /tmp/star.trek.9/star.trek.9.mkv +: type: movie + title: star trek 9 + +? star.trek.9.mkv +: type: movie + title: star trek 9 + +? FlexGet.S01E02.TheName.HDTV.xvid +: episode: 2 + source: HDTV + season: 1 + title: FlexGet + episode_title: TheName + type: episode + video_codec: Xvid + +? FlexGet.S01E02.TheName.HDTV.xvid +: episode: 2 + source: HDTV + season: 1 + title: FlexGet + episode_title: TheName + type: episode + video_codec: Xvid + +? some.series.S03E14.Title.Here.720p +: episode: 14 + screen_size: 720p + season: 3 + title: some series + episode_title: Title Here + type: episode + +? '[the.group] Some.Series.S03E15.Title.Two.720p' +: episode: 15 + release_group: the.group + screen_size: 720p + season: 3 + title: Some Series + episode_title: Title Two + type: episode + +? 'HD 720p: Some series.S03E16.Title.Three' +: episode: 16 + other: HD + screen_size: 720p + season: 3 + title: Some series + episode_title: Title Three + type: episode + +? Something.Season.2.1of4.Ep.Title.HDTV.torrent +: episode_count: 4 + episode: 1 + source: HDTV + season: 2 + title: Something + episode_title: Title + type: episode + container: torrent + +? Show-A (US) - Episode Title S02E09 hdtv +: country: US + episode: 9 + source: HDTV + season: 2 + title: Show-A + type: episode + +? Jack's.Show.S03E01.blah.1080p +: episode: 1 + screen_size: 1080p + season: 3 + title: Jack's Show + episode_title: blah + type: episode + +? FlexGet.epic +: title: FlexGet epic + type: movie + +? FlexGet.Apt.1 +: title: FlexGet Apt 1 + type: movie + +? FlexGet.aptitude +: title: FlexGet aptitude + type: movie + +? FlexGet.Step1 +: title: FlexGet Step1 + type: movie + +? Movies/El Bosque Animado (1987)/El.Bosque.Animado.[Jose.Luis.Cuerda.1987].[Xvid-Dvdrip-720 * 432].avi +: source: DVD + other: Rip + screen_size: 720x432 + title: El Bosque Animado + video_codec: Xvid + year: 1987 + type: movie + +? Movies/El Bosque Animado (1987)/El.Bosque.Animado.[Jose.Luis.Cuerda.1987].[Xvid-Dvdrip-720x432].avi +: source: DVD + other: Rip + screen_size: 720x432 + title: El Bosque Animado + video_codec: Xvid + year: 1987 + type: movie + +? 2009.shoot.fruit.chan.multi.dvd9.pal +: source: DVD + language: mul + other: PAL + title: shoot fruit chan + type: movie + year: 2009 + +? 2009.shoot.fruit.chan.multi.dvd5.pal +: source: DVD + language: mul + other: PAL + title: shoot fruit chan + type: movie + year: 2009 + +? The.Flash.2014.S01E01.PREAIR.WEBRip.XviD-EVO.avi +: episode: 1 + source: Web + other: [Preair, Rip] + release_group: EVO + season: 1 + title: The Flash + type: episode + video_codec: Xvid + year: 2014 + +? Ice.Lake.Rebels.S01E06.Ice.Lake.Games.720p.HDTV.x264-DHD +: episode: 6 + source: HDTV + release_group: DHD + screen_size: 720p + season: 1 + title: Ice Lake Rebels + episode_title: Ice Lake Games + type: episode + video_codec: H.264 + +? The League - S06E10 - Epi Sexy.mkv +: episode: 10 + season: 6 + title: The League + episode_title: Epi Sexy + type: episode + +? Stay (2005) [1080p]/Stay.2005.1080p.BluRay.x264.YIFY.mp4 +: source: Blu-ray + release_group: YIFY + screen_size: 1080p + title: Stay + type: movie + video_codec: H.264 + year: 2005 + +? /media/live/A/Anger.Management.S02E82.720p.HDTV.X264-DIMENSION.mkv +: source: HDTV + release_group: DIMENSION + screen_size: 720p + title: Anger Management + type: episode + season: 2 + episode: 82 + video_codec: H.264 + +? "[Figmentos] Monster 34 - At the End of Darkness [781219F1].mkv" +: type: episode + release_group: Figmentos + title: Monster + episode: 34 + episode_title: At the End of Darkness + crc32: 781219F1 + +? Game.of.Thrones.S05E07.720p.HDTV-KILLERS.mkv +: type: episode + episode: 7 + source: HDTV + release_group: KILLERS + screen_size: 720p + season: 5 + title: Game of Thrones + +? Game.of.Thrones.S05E07.HDTV.720p-KILLERS.mkv +: type: episode + episode: 7 + source: HDTV + release_group: KILLERS + screen_size: 720p + season: 5 + title: Game of Thrones + +? Parks and Recreation - [04x12] - Ad Campaign.avi +: type: episode + title: Parks and Recreation + season: 4 + episode: 12 + episode_title: Ad Campaign + +? Star Trek Into Darkness (2013)/star.trek.into.darkness.2013.720p.web-dl.h264-publichd.mkv +: type: movie + title: Star Trek Into Darkness + year: 2013 + screen_size: 720p + source: Web + video_codec: H.264 + release_group: publichd + +? /var/medias/series/The Originals/Season 02/The.Originals.S02E15.720p.HDTV.X264-DIMENSION.mkv +: type: episode + title: The Originals + season: 2 + episode: 15 + screen_size: 720p + source: HDTV + video_codec: H.264 + release_group: DIMENSION + +? Test.S01E01E07-FooBar-Group.avi +: container: avi + episode: + - 1 + - 7 + episode_title: FooBar-Group # Make sure it doesn't conflict with uuid + season: 1 + title: Test + type: episode + +? TEST.S01E02.2160p.NF.WEBRip.x264.DD5.1-ABC +: audio_channels: '5.1' + audio_codec: Dolby Digital + episode: 2 + source: Web + other: Rip + release_group: ABC + screen_size: 2160p + season: 1 + streaming_service: Netflix + title: TEST + type: episode + video_codec: H.264 + +? TEST.2015.12.30.720p.WEBRip.h264-ABC +: date: 2015-12-30 + source: Web + other: Rip + release_group: ABC + screen_size: 720p + title: TEST + type: episode + video_codec: H.264 + +? TEST.S01E10.24.1080p.NF.WEBRip.AAC2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 10 + episode_title: '24' + source: Web + other: Rip + release_group: ABC + screen_size: 1080p + season: 1 + streaming_service: Netflix + title: TEST + type: episode + video_codec: H.264 + +? TEST.S01E10.24.1080p.NF.WEBRip.AAC2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 10 + episode_title: '24' + source: Web + other: Rip + release_group: ABC + screen_size: 1080p + season: 1 + streaming_service: Netflix + title: TEST + type: episode + video_codec: H.264 + +? TEST.S01E10.24.1080p.NF.WEBRip.AAC.2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 10 + episode_title: '24' + source: Web + other: Rip + release_group: ABC + screen_size: 1080p + season: 1 + streaming_service: Netflix + title: TEST + type: episode + video_codec: H.264 + +? TEST.S05E02.720p.iP.WEBRip.AAC2.0.H264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 2 + source: Web + other: Rip + release_group: ABC + screen_size: 720p + season: 5 + title: TEST + type: episode + video_codec: H.264 + +? TEST.S03E07.720p.WEBRip.AAC2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 7 + source: Web + other: Rip + release_group: ABC + screen_size: 720p + season: 3 + title: TEST + type: episode + video_codec: H.264 + +? TEST.S15E15.24.1080p.FREE.WEBRip.AAC2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 15 + episode_title: '24' + source: Web + other: Rip + release_group: ABC + screen_size: 1080p + season: 15 + title: TEST + type: episode + video_codec: H.264 + +? TEST.S11E11.24.720p.ETV.WEBRip.AAC2.0.x264-ABC +: audio_channels: '2.0' + audio_codec: AAC + episode: 11 + episode_title: '24' + source: Web + other: Rip + release_group: ABC + screen_size: 720p + season: 11 + title: TEST + type: episode + video_codec: H.264 + +? TEST.2015.1080p.HC.WEBRip.x264.AAC2.0-ABC +: audio_channels: '2.0' + audio_codec: AAC + source: Web + other: Rip + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + video_codec: H.264 + year: 2015 + +? TEST.2015.1080p.3D.BluRay.Half-SBS.x264.DTS-HD.MA.7.1-ABC +: audio_channels: '7.1' + audio_codec: DTS-HD + audio_profile: Master Audio + source: Blu-ray + other: 3D + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + video_codec: H.264 + year: 2015 + +? TEST.2015.1080p.3D.BluRay.Half-OU.x264.DTS-HD.MA.7.1-ABC +: audio_channels: '7.1' + audio_codec: DTS-HD + audio_profile: Master Audio + source: Blu-ray + other: 3D + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + video_codec: H.264 + year: 2015 + +? TEST.2015.1080p.3D.BluRay.Half-OU.x264.DTS-HD.MA.TrueHD.7.1.Atmos-ABC +: audio_channels: '7.1' + audio_codec: + - DTS-HD + - Dolby TrueHD + - Dolby Atmos + audio_profile: Master Audio + source: Blu-ray + other: 3D + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + video_codec: H.264 + year: 2015 + +? TEST.2015.1080p.3D.BluRay.Half-SBS.x264.DTS-HD.MA.TrueHD.7.1.Atmos-ABC +: audio_channels: '7.1' + audio_codec: + - DTS-HD + - Dolby TrueHD + - Dolby Atmos + audio_profile: Master Audio + source: Blu-ray + other: 3D + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + video_codec: H.264 + year: 2015 + +? TEST.2015.1080p.BluRay.REMUX.AVC.DTS-HD.MA.TrueHD.7.1.Atmos-ABC +: audio_channels: '7.1' + audio_codec: + - DTS-HD + - Dolby TrueHD + - Dolby Atmos + audio_profile: Master Audio + source: Blu-ray + other: Remux + release_group: ABC + screen_size: 1080p + title: TEST + type: movie + year: 2015 + +? Gangs of New York 2002 REMASTERED 1080p BluRay x264-AVCHD +: source: Blu-ray + edition: Remastered + screen_size: 1080p + title: Gangs of New York + type: movie + video_codec: H.264 + year: 2002 + +? Peep.Show.S06E02.DVDrip.x264-faks86.mkv +: container: mkv + episode: 2 + source: DVD + other: Rip + release_group: faks86 + season: 6 + title: Peep Show + type: episode + video_codec: H.264 + +# Episode title is indeed 'October 8, 2014' +# https://thetvdb.com/?tab=episode&seriesid=82483&seasonid=569935&id=4997362&lid=7 +? The Soup - 11x41 - October 8, 2014.mp4 +: container: mp4 + episode: 41 + episode_title: October 8, 2014 + season: 11 + title: The Soup + type: episode + +? Red.Rock.S02E59.WEB-DLx264-JIVE +: episode: 59 + season: 2 + source: Web + release_group: JIVE + title: Red Rock + type: episode + video_codec: H.264 + +? Pawn.Stars.S12E31.Deals.On.Wheels.PDTVx264-JIVE +: episode: 31 + episode_title: Deals On Wheels + season: 12 + source: Digital TV + release_group: JIVE + title: Pawn Stars + type: episode + video_codec: H.264 + +? Duck.Dynasty.S09E09.Van.He-llsing.HDTVx264-JIVE +: episode: 9 + episode_title: Van He-llsing + season: 9 + source: HDTV + release_group: JIVE + title: Duck Dynasty + type: episode + video_codec: H.264 + +? ATKExotics.16.01.24.Ava.Alba.Watersports.XXX.1080p.MP4-KTR +: title: ATKExotics + episode_title: Ava Alba Watersports + other: XXX + screen_size: 1080p + container: mp4 + release_group: KTR + type: episode + +? PutaLocura.15.12.22.Spanish.Luzzy.XXX.720p.MP4-oRo +: title: PutaLocura + episode_title: Spanish Luzzy + other: XXX + screen_size: 720p + container: mp4 + release_group: oRo + type: episode + +? French Maid Services - Lola At Your Service WEB-DL SPLIT SCENES MP4-RARBG +: title: French Maid Services + alternative_title: Lola At Your Service + source: Web + container: mp4 + release_group: RARBG + type: movie + +? French Maid Services - Lola At Your Service - Marc Dorcel WEB-DL SPLIT SCENES MP4-RARBG +: title: French Maid Services + alternative_title: [Lola At Your Service, Marc Dorcel] + source: Web + container: mp4 + release_group: RARBG + type: movie + +? PlayboyPlus.com_16.01.23.Eleni.Corfiate.Playboy.Romania.XXX.iMAGESET-OHRLY +: episode_title: Eleni Corfiate Playboy Romania + other: XXX + type: episode + +? TeenPornoPass - Anna - Beautiful Ass Deep Penetrated 720p mp4 +: title: TeenPornoPass + alternative_title: + - Anna + - Beautiful Ass Deep Penetrated + screen_size: 720p + container: mp4 + type: movie + +? SexInJeans.Gina.Gerson.Super.Nasty.Asshole.Pounding.With.Gina.In.Jeans.A.Devil.In.Denim.The.Finest.Ass.Fuck.Frolicking.mp4 +: title: SexInJeans Gina Gerson Super Nasty Asshole Pounding With Gina In Jeans A Devil In Denim The Finest Ass Fuck Frolicking + container: mp4 + type: movie + +? TNA Impact Wrestling HDTV 2017-06-22 720p H264 AVCHD-SC-SDH +: title: TNA Impact Wrestling + source: HDTV + date: 2017-06-22 + screen_size: 720p + video_codec: H.264 + release_group: SDH + type: episode + +? Katy Perry - Pepsi & Billboard Summer Beats Concert Series 2012 1080i HDTV 20 Mbps DD2.0 MPEG2-TrollHD.ts +: title: Katy Perry + alternative_title: Pepsi & Billboard Summer Beats Concert + year: 2012 + screen_size: 1080i + source: HDTV + video_bit_rate: 20Mbps + audio_codec: Dolby Digital + audio_channels: '2.0' + video_codec: MPEG-2 + release_group: TrollHD + container: ts + +? Justin Timberlake - MTV Video Music Awards 2013 1080i 32 Mbps DTS-HD 5.1.ts +: title: Justin Timberlake + alternative_title: MTV Video Music Awards + year: 2013 + screen_size: 1080i + video_bit_rate: 32Mbps + audio_codec: DTS-HD + audio_channels: '5.1' + container: ts + type: movie + +? Chuck Berry The Very Best Of Chuck Berry(2010)[320 Kbps] +: title: Chuck Berry The Very Best Of Chuck Berry + year: 2010 + audio_bit_rate: 320Kbps + type: movie + +? Title Name [480p][1.5Mbps][.mp4] +: title: Title Name + screen_size: 480p + video_bit_rate: 1.5Mbps + container: mp4 + type: movie + +? This.is.Us +: options: --no-embedded-config + title: This is Us + type: movie + +? This.is.Us +: options: --excludes country + title: This is Us + type: movie + +? MotoGP.2016x03.USA.Race.BTSportHD.1080p25 +: title: MotoGP + season: 2016 + year: 2016 + episode: 3 + screen_size: 1080p + frame_rate: 25fps + type: episode + +? BBC.Earth.South.Pacific.2010.D2.1080p.24p.BD25.DTS-HD +: title: BBC Earth South Pacific + year: 2010 + screen_size: 1080p + frame_rate: 24fps + source: Blu-ray + audio_codec: DTS-HD + type: movie diff --git a/libs/guessit/tlds-alpha-by-domain.txt b/libs/guessit/tlds-alpha-by-domain.txt new file mode 100644 index 000000000..280c794c5 --- /dev/null +++ b/libs/guessit/tlds-alpha-by-domain.txt @@ -0,0 +1,341 @@ +# Version 2013112900, Last Updated Fri Nov 29 07:07:01 2013 UTC +AC +AD +AE +AERO +AF +AG +AI +AL +AM +AN +AO +AQ +AR +ARPA +AS +ASIA +AT +AU +AW +AX +AZ +BA +BB +BD +BE +BF +BG +BH +BI +BIKE +BIZ +BJ +BM +BN +BO +BR +BS +BT +BV +BW +BY +BZ +CA +CAMERA +CAT +CC +CD +CF +CG +CH +CI +CK +CL +CLOTHING +CM +CN +CO +COM +CONSTRUCTION +CONTRACTORS +COOP +CR +CU +CV +CW +CX +CY +CZ +DE +DIAMONDS +DIRECTORY +DJ +DK +DM +DO +DZ +EC +EDU +EE +EG +ENTERPRISES +EQUIPMENT +ER +ES +ESTATE +ET +EU +FI +FJ +FK +FM +FO +FR +GA +GALLERY +GB +GD +GE +GF +GG +GH +GI +GL +GM +GN +GOV +GP +GQ +GR +GRAPHICS +GS +GT +GU +GURU +GW +GY +HK +HM +HN +HOLDINGS +HR +HT +HU +ID +IE +IL +IM +IN +INFO +INT +IO +IQ +IR +IS +IT +JE +JM +JO +JOBS +JP +KE +KG +KH +KI +KITCHEN +KM +KN +KP +KR +KW +KY +KZ +LA +LAND +LB +LC +LI +LIGHTING +LK +LR +LS +LT +LU +LV +LY +MA +MC +MD +ME +MG +MH +MIL +MK +ML +MM +MN +MO +MOBI +MP +MQ +MR +MS +MT +MU +MUSEUM +MV +MW +MX +MY +MZ +NA +NAME +NC +NE +NET +NF +NG +NI +NL +NO +NP +NR +NU +NZ +OM +ORG +PA +PE +PF +PG +PH +PHOTOGRAPHY +PK +PL +PLUMBING +PM +PN +POST +PR +PRO +PS +PT +PW +PY +QA +RE +RO +RS +RU +RW +SA +SB +SC +SD +SE +SEXY +SG +SH +SI +SINGLES +SJ +SK +SL +SM +SN +SO +SR +ST +SU +SV +SX +SY +SZ +TATTOO +TC +TD +TECHNOLOGY +TEL +TF +TG +TH +TIPS +TJ +TK +TL +TM +TN +TO +TODAY +TP +TR +TRAVEL +TT +TV +TW +TZ +UA +UG +UK +US +UY +UZ +VA +VC +VE +VENTURES +VG +VI +VN +VOYAGE +VU +WF +WS +XN--3E0B707E +XN--45BRJ9C +XN--80AO21A +XN--80ASEHDB +XN--80ASWG +XN--90A3AC +XN--CLCHC0EA0B2G2A9GCD +XN--FIQS8S +XN--FIQZ9S +XN--FPCRJ9C3D +XN--FZC2C9E2C +XN--GECRJ9C +XN--H2BRJ9C +XN--J1AMH +XN--J6W193G +XN--KPRW13D +XN--KPRY57D +XN--L1ACC +XN--LGBBAT1AD8J +XN--MGB9AWBF +XN--MGBA3A4F16A +XN--MGBAAM7A8H +XN--MGBAYH7GPA +XN--MGBBH1A71E +XN--MGBC0A9AZCG +XN--MGBERP4A5D4AR +XN--MGBX4CD0AB +XN--NGBC5AZD +XN--O3CW4H +XN--OGBPF8FL +XN--P1AI +XN--PGBS0DH +XN--Q9JYB4C +XN--S9BRJ9C +XN--UNUP4Y +XN--WGBH1C +XN--WGBL6A +XN--XKC2AL3HYE2A +XN--XKC2DL3A5EE0H +XN--YFRO4I67O +XN--YGBI2AMMX +XXX +YE +YT +ZA +ZM +ZW diff --git a/libs/guessit/yamlutils.py b/libs/guessit/yamlutils.py new file mode 100644 index 000000000..01ac77781 --- /dev/null +++ b/libs/guessit/yamlutils.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Options +""" + +try: + from collections import OrderedDict +except ImportError: # pragma: no-cover + from ordereddict import OrderedDict # pylint:disable=import-error +import babelfish + +import yaml + +from .rules.common.quantity import BitRate, FrameRate, Size + + +class OrderedDictYAMLLoader(yaml.Loader): + """ + A YAML loader that loads mappings into ordered dictionaries. + From https://gist.github.com/enaeseth/844388 + """ + + def __init__(self, *args, **kwargs): + yaml.Loader.__init__(self, *args, **kwargs) + + self.add_constructor(u'tag:yaml.org,2002:map', type(self).construct_yaml_map) + self.add_constructor(u'tag:yaml.org,2002:omap', type(self).construct_yaml_map) + + def construct_yaml_map(self, node): + data = OrderedDict() + yield data + value = self.construct_mapping(node) + data.update(value) + + def construct_mapping(self, node, deep=False): + if isinstance(node, yaml.MappingNode): + self.flatten_mapping(node) + else: # pragma: no cover + raise yaml.constructor.ConstructorError(None, None, + 'expected a mapping node, but found %s' % node.id, node.start_mark) + + mapping = OrderedDict() + for key_node, value_node in node.value: + key = self.construct_object(key_node, deep=deep) + try: + hash(key) + except TypeError as exc: # pragma: no cover + raise yaml.constructor.ConstructorError('while constructing a mapping', + node.start_mark, 'found unacceptable key (%s)' + % exc, key_node.start_mark) + value = self.construct_object(value_node, deep=deep) + mapping[key] = value + return mapping + + +class CustomDumper(yaml.SafeDumper): + """ + Custom YAML Dumper. + """ + pass + + +def default_representer(dumper, data): + """Default representer""" + return dumper.represent_str(str(data)) + + +CustomDumper.add_representer(babelfish.Language, default_representer) +CustomDumper.add_representer(babelfish.Country, default_representer) +CustomDumper.add_representer(BitRate, default_representer) +CustomDumper.add_representer(FrameRate, default_representer) +CustomDumper.add_representer(Size, default_representer) + + +def ordered_dict_representer(dumper, data): + """OrderedDict representer""" + return dumper.represent_mapping('tag:yaml.org,2002:map', data.items()) + + +CustomDumper.add_representer(OrderedDict, ordered_dict_representer) |