summaryrefslogtreecommitdiffhomepage
path: root/libs/guessit
diff options
context:
space:
mode:
authormorpheus65535 <[email protected]>2018-09-16 20:27:00 -0400
committermorpheus65535 <[email protected]>2018-09-16 20:33:04 -0400
commit0f061f21226f91883c841f85ceef31b30981277a (patch)
treea1350723ae688ccbae4d4ca564cc4175ccc73996 /libs/guessit
parent8b681d8a151a3b41d3aaa5bfdd7a082bdda7896c (diff)
downloadbazarr-0f061f21226f91883c841f85ceef31b30981277a.tar.gz
bazarr-0f061f21226f91883c841f85ceef31b30981277a.zip
Include dependencies and remove requirements.txt
Diffstat (limited to 'libs/guessit')
-rw-r--r--libs/guessit/__init__.py10
-rw-r--r--libs/guessit/__main__.py170
-rw-r--r--libs/guessit/__version__.py7
-rw-r--r--libs/guessit/api.py202
-rw-r--r--libs/guessit/backports.py27
-rw-r--r--libs/guessit/config/options.json363
-rw-r--r--libs/guessit/jsonutils.py35
-rw-r--r--libs/guessit/options.py262
-rw-r--r--libs/guessit/reutils.py35
-rw-r--r--libs/guessit/rules/__init__.py99
-rw-r--r--libs/guessit/rules/common/__init__.py15
-rw-r--r--libs/guessit/rules/common/comparators.py75
-rw-r--r--libs/guessit/rules/common/date.py125
-rw-r--r--libs/guessit/rules/common/expected.py53
-rw-r--r--libs/guessit/rules/common/formatters.py136
-rw-r--r--libs/guessit/rules/common/numeral.py165
-rw-r--r--libs/guessit/rules/common/pattern.py27
-rw-r--r--libs/guessit/rules/common/quantity.py106
-rw-r--r--libs/guessit/rules/common/validators.py51
-rw-r--r--libs/guessit/rules/common/words.py34
-rw-r--r--libs/guessit/rules/markers/__init__.py5
-rw-r--r--libs/guessit/rules/markers/groups.py52
-rw-r--r--libs/guessit/rules/markers/path.py47
-rw-r--r--libs/guessit/rules/processors.py240
-rw-r--r--libs/guessit/rules/properties/__init__.py5
-rw-r--r--libs/guessit/rules/properties/audio_codec.py209
-rw-r--r--libs/guessit/rules/properties/bit_rate.py72
-rw-r--r--libs/guessit/rules/properties/bonus.py55
-rw-r--r--libs/guessit/rules/properties/cds.py41
-rw-r--r--libs/guessit/rules/properties/container.py60
-rw-r--r--libs/guessit/rules/properties/country.py114
-rw-r--r--libs/guessit/rules/properties/crc.py90
-rw-r--r--libs/guessit/rules/properties/date.py81
-rw-r--r--libs/guessit/rules/properties/edition.py52
-rw-r--r--libs/guessit/rules/properties/episode_title.py288
-rw-r--r--libs/guessit/rules/properties/episodes.py861
-rw-r--r--libs/guessit/rules/properties/film.py48
-rw-r--r--libs/guessit/rules/properties/language.py503
-rw-r--r--libs/guessit/rules/properties/mimetype.py55
-rw-r--r--libs/guessit/rules/properties/other.py344
-rw-r--r--libs/guessit/rules/properties/part.py46
-rw-r--r--libs/guessit/rules/properties/release_group.py327
-rw-r--r--libs/guessit/rules/properties/screen_size.py163
-rw-r--r--libs/guessit/rules/properties/size.py30
-rw-r--r--libs/guessit/rules/properties/source.py201
-rw-r--r--libs/guessit/rules/properties/streaming_service.py198
-rw-r--r--libs/guessit/rules/properties/title.py332
-rw-r--r--libs/guessit/rules/properties/type.py83
-rw-r--r--libs/guessit/rules/properties/video_codec.py115
-rw-r--r--libs/guessit/rules/properties/website.py103
-rw-r--r--libs/guessit/test/__init__.py3
-rw-r--r--libs/guessit/test/config/dummy.txt1
-rw-r--r--libs/guessit/test/config/test.json4
-rw-r--r--libs/guessit/test/config/test.yaml4
-rw-r--r--libs/guessit/test/config/test.yml4
-rw-r--r--libs/guessit/test/enable_disable_properties.yml335
-rw-r--r--libs/guessit/test/episodes.yml4496
-rw-r--r--libs/guessit/test/movies.yml1721
-rw-r--r--libs/guessit/test/rules/__init__.py3
-rw-r--r--libs/guessit/test/rules/audio_codec.yml131
-rw-r--r--libs/guessit/test/rules/bonus.yml9
-rw-r--r--libs/guessit/test/rules/cds.yml10
-rw-r--r--libs/guessit/test/rules/country.yml13
-rw-r--r--libs/guessit/test/rules/date.yml50
-rw-r--r--libs/guessit/test/rules/edition.yml63
-rw-r--r--libs/guessit/test/rules/episodes.yml279
-rw-r--r--libs/guessit/test/rules/film.yml9
-rw-r--r--libs/guessit/test/rules/language.yml47
-rw-r--r--libs/guessit/test/rules/other.yml172
-rw-r--r--libs/guessit/test/rules/part.yml18
-rw-r--r--libs/guessit/test/rules/processors.yml8
-rw-r--r--libs/guessit/test/rules/processors_test.py46
-rw-r--r--libs/guessit/test/rules/release_group.yml71
-rw-r--r--libs/guessit/test/rules/screen_size.yml259
-rw-r--r--libs/guessit/test/rules/size.yml8
-rw-r--r--libs/guessit/test/rules/source.yml323
-rw-r--r--libs/guessit/test/rules/title.yml32
-rw-r--r--libs/guessit/test/rules/video_codec.yml87
-rw-r--r--libs/guessit/test/rules/website.yml23
-rw-r--r--libs/guessit/test/streaming_services.yaml1934
-rw-r--r--libs/guessit/test/test-input-file.txt2
-rw-r--r--libs/guessit/test/test_api.py71
-rw-r--r--libs/guessit/test/test_api_unicode_literals.py74
-rw-r--r--libs/guessit/test/test_benchmark.py52
-rw-r--r--libs/guessit/test/test_main.py72
-rw-r--r--libs/guessit/test/test_options.py142
-rw-r--r--libs/guessit/test/test_yml.py288
-rw-r--r--libs/guessit/test/various.yml944
-rw-r--r--libs/guessit/tlds-alpha-by-domain.txt341
-rw-r--r--libs/guessit/yamlutils.py81
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 &amp; Bars (2009) S01E01 720p XviD-2HD[AOEU]
+: episode: 1
+ release_group: 2HD[AOEU]
+ screen_size: 720p
+ season: 1
+ title: Foo's &amp; 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
+
+: 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)