Merge branch 'development' into daemon

# Conflicts:
#	bazarr.py
#	bazarr/get_languages.py
pull/222/head
Louis Vézina 6 years ago
commit 3e17afc028

@ -31,7 +31,7 @@ def language_from_alpha2(lang):
result = c.execute('''SELECT name FROM table_settings_languages WHERE code2 = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
def language_from_alpha3(lang):
@ -43,7 +43,7 @@ def language_from_alpha3(lang):
result = c.execute('''SELECT name FROM table_settings_languages WHERE code3 = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
def alpha2_from_alpha3(lang):
@ -55,7 +55,7 @@ def alpha2_from_alpha3(lang):
result = c.execute('''SELECT code2 FROM table_settings_languages WHERE code3 = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
def alpha2_from_language(lang):
@ -65,7 +65,7 @@ def alpha2_from_language(lang):
result = c.execute('''SELECT code2 FROM table_settings_languages WHERE name = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
def alpha3_from_alpha2(lang):
@ -75,7 +75,7 @@ def alpha3_from_alpha2(lang):
result = c.execute('''SELECT code3 FROM table_settings_languages WHERE code2 = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
def alpha3_from_language(lang):
@ -85,8 +85,8 @@ def alpha3_from_language(lang):
result = c.execute('''SELECT code3 FROM table_settings_languages WHERE name = ?''', (lang,)).fetchone()[0]
except:
result = None
db.close
db.close()
return result
if __name__ == '__main__':
load_language_in_db()
load_language_in_db()

@ -106,7 +106,8 @@ def update_movies():
db = sqlite3.connect(os.path.join(config_dir, 'db/bazarr.db'), timeout=30)
c = db.cursor()
c.executemany('DELETE FROM table_movies WHERE tmdbId = ?', removed_movies)
for removed_movie in removed_movies:
c.execute('DELETE FROM table_movies WHERE tmdbId = ?', (removed_movie,))
db.commit()
db.close()

@ -105,9 +105,9 @@ def get_general_settings():
serie_default_language = []
if cfg.has_option('general', 'serie_default_hi'):
serie_default_hi = cfg.getboolean('general', 'serie_default_hi')
serie_default_hi = cfg.get('general', 'serie_default_hi')
else:
serie_default_hi = False
serie_default_hi = 'False'
if cfg.has_option('general', 'movie_default_enabled'):
movie_default_enabled = cfg.getboolean('general', 'movie_default_enabled')
@ -120,9 +120,9 @@ def get_general_settings():
movie_default_language = []
if cfg.has_option('general', 'movie_default_hi'):
movie_default_hi = cfg.getboolean('general', 'movie_default_hi')
movie_default_hi = cfg.get('general', 'movie_default_hi')
else:
movie_default_hi = False
movie_default_hi = 'False'
if cfg.has_option('general', 'page_size'):
page_size = cfg.get('general', 'page_size')
@ -161,16 +161,16 @@ def get_general_settings():
minimum_score = '90'
use_scenename = False
use_postprocessing = False
postprocessing_cmd = False
postprocessing_cmd = ''
use_sonarr = False
use_radarr = False
path_mappings_movie = []
serie_default_enabled = False
serie_default_language = []
serie_default_hi = False
serie_default_hi = 'False'
movie_default_enabled = False
movie_default_language = []
movie_default_hi = False
movie_default_hi = 'False'
page_size = '25'
minimum_score_movie = '70'
use_embedded_subs = False

@ -4,6 +4,7 @@ import os
import sqlite3
import ast
import logging
import operator
import subprocess
import time
from datetime import datetime, timedelta
@ -28,95 +29,122 @@ def download_subtitle(path, language, hi, providers, providers_auth, sceneName,
hi = True
else:
hi = False
if media_type == 'series':
type_of_score = 360
minimum_score = float(get_general_settings()[8]) / 100 * type_of_score
elif media_type == 'movie':
type_of_score = 120
minimum_score = float(get_general_settings()[22]) / 100 * type_of_score
language_set = set()
if language == 'pob':
language_set.add(Language('por', 'BR'))
else:
language_set.add(Language(language))
use_scenename = get_general_settings()[9]
minimum_score = get_general_settings()[8]
minimum_score_movie = get_general_settings()[22]
use_postprocessing = get_general_settings()[10]
postprocessing_cmd = get_general_settings()[11]
if language == 'pob':
lang_obj = Language('por', 'BR')
else:
lang_obj = Language(language)
try:
if sceneName is None or use_scenename is False:
if sceneName == "None" or use_scenename is False:
used_sceneName = False
video = scan_video(path)
else:
used_sceneName = True
video = Video.fromname(sceneName)
except Exception as e:
logging.exception('Error trying to extract information from this filename: ' + path)
return None
logging.exception("Error trying to get video information for this file: " + path)
else:
if media_type == "movie":
max_score = 120.0
elif media_type == "series":
max_score = 360.0
try:
best_subtitles = download_best_subtitles([video], {lang_obj}, providers=providers, min_score=minimum_score, hearing_impaired=hi, provider_configs=providers_auth)
with AsyncProviderPool(max_workers=None, providers=providers, provider_configs=providers_auth) as p:
subtitles = p.list_subtitles(video, language_set)
except Exception as e:
logging.exception('Error trying to get the best subtitles for this file: ' + path)
return None
logging.exception("Error trying to get subtitle list from provider")
else:
try:
best_subtitle = best_subtitles[video][0]
except:
logging.debug('No subtitles found for ' + path)
return None
else:
single = get_general_settings()[7]
subtitles_list = []
sorted_subtitles = sorted([(s, compute_score(s, video, hearing_impaired=hi)) for s in subtitles], key=operator.itemgetter(1), reverse=True)
for s, preliminary_score in sorted_subtitles:
if media_type == "movie":
if (preliminary_score / max_score * 100) < int(minimum_score_movie):
continue
matched = set(s.get_matches(video))
if hi == s.hearing_impaired:
matched.add('hearing_impaired')
not_matched = set(score.movie_scores.keys()) - matched
required = set(['title'])
if any(elem in required for elem in not_matched):
continue
elif media_type == "series":
if (preliminary_score / max_score * 100) < int(minimum_score):
continue
matched = set(s.get_matches(video))
if hi == s.hearing_impaired:
matched.add('hearing_impaired')
not_matched = set(score.episode_scores.keys()) - matched
required = set(['series', 'season', 'episode'])
if any(elem in required for elem in not_matched):
continue
subtitles_list.append(s)
if len(subtitles_list) > 0:
best_subtitle = subtitles_list[0]
download_subtitles([best_subtitle], providers=providers, provider_configs=providers_auth)
try:
score = round(float(compute_score(best_subtitle, video, hearing_impaired=hi)) / type_of_score * 100, 2)
calculated_score = round(float(compute_score(best_subtitle, video, hearing_impaired=hi)) / max_score * 100, 2)
if used_sceneName == True:
video = scan_video(path)
single = get_general_settings()[7]
if single is True:
result = save_subtitles(video, [best_subtitle], single=True, encoding='utf-8')
else:
result = save_subtitles(video, [best_subtitle], encoding='utf-8')
except:
logging.error('Error saving subtitles file to disk.')
except Exception as e:
logging.exception('Error saving subtitles file to disk.')
return None
else:
downloaded_provider = str(result[0]).strip('<>').split(' ')[0][:-8]
downloaded_language = language_from_alpha3(language)
downloaded_language_code2 = alpha2_from_alpha3(language)
downloaded_language_code3 = language
downloaded_path = get_subtitle_path(path, language=lang_obj)
if used_sceneName == True:
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(score) + "% using this scene name: " + sceneName
else:
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(score) + "% using filename guessing."
if use_postprocessing is True:
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3)
try:
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if len(result) > 0:
downloaded_provider = result[0].provider_name
downloaded_language = language_from_alpha3(result[0].language.alpha3)
downloaded_language_code2 = alpha2_from_alpha3(result[0].language.alpha3)
downloaded_language_code3 = result[0].language.alpha3
downloaded_path = get_subtitle_path(path, language=language_set)
if used_sceneName == True:
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(calculated_score) + "% using this scene name: " + sceneName
else:
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(calculated_score) + "% using filename guessing."
if use_postprocessing is True:
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3)
try:
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
out, err = process.communicate()
if os.name == 'nt':
out = out.decode(encoding)
if os.name == 'nt':
out = out.decode(encoding)
except:
if out == "":
logging.error('Post-processing result for file ' + path + ' : Nothing returned from command execution')
except:
if out == "":
logging.error('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.error('Post-processing result for file ' + path + ' : ' + out)
else:
logging.error('Post-processing result for file ' + path + ' : ' + out)
else:
if out == "":
logging.info('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('Post-processing result for file ' + path + ' : ' + out)
if out == "":
logging.info('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('Post-processing result for file ' + path + ' : ' + out)
return message
return message
else:
return None
else:
return None
def manual_search(path, language, hi, providers, providers_auth, sceneName, media_type):
if hi == "True":
@ -125,15 +153,23 @@ def manual_search(path, language, hi, providers, providers_auth, sceneName, medi
hi = False
language_set = set()
for lang in ast.literal_eval(language):
if lang == 'pb':
lang = alpha3_from_alpha2(lang)
if lang == 'pob':
language_set.add(Language('por', 'BR'))
else:
language_set.add(Language(alpha3_from_alpha2(lang)))
language_set.add(Language(lang))
use_scenename = get_general_settings()[9]
use_postprocessing = get_general_settings()[10]
postprocessing_cmd = get_general_settings()[11]
try:
if sceneName != "None":
video = Video.fromname(sceneName)
else:
if sceneName == "None" or use_scenename is False:
used_sceneName = False
video = scan_video(path)
else:
used_sceneName = True
video = Video.fromname(sceneName)
except:
logging.error("Error trying to get video information.")
else:
@ -156,15 +192,21 @@ def manual_search(path, language, hi, providers, providers_auth, sceneName, medi
if hi == s.hearing_impaired:
matched.add('hearing_impaired')
not_matched = set(score.movie_scores.keys()) - matched
if "title" in not_matched:
required = set(['title'])
if any(elem in required for elem in not_matched):
continue
if used_sceneName:
not_matched.remove('hash')
elif media_type == "series":
matched = set(s.get_matches(video))
if hi == s.hearing_impaired:
matched.add('hearing_impaired')
not_matched = set(score.episode_scores.keys()) - matched
if "series" in not_matched or "season" in not_matched or "episode" in not_matched:
required = set(['series', 'season', 'episode'])
if any(elem in required for elem in not_matched):
continue
if used_sceneName:
not_matched.remove('hash')
subtitles_list.append(dict(score=round((compute_score(s, video, hearing_impaired=hi) / max_score * 100), 2), language=alpha2_from_alpha3(s.language.alpha3), hearing_impaired=str(s.hearing_impaired), provider=s.provider_name, subtitle=codecs.encode(pickle.dumps(s), "base64").decode(), url=s.page_link, matches=list(matched), dont_matches=list(not_matched)))
subtitles_dict = {}
subtitles_dict = sorted(subtitles_list, key=lambda x: x['score'], reverse=True)
@ -184,11 +226,10 @@ def manual_download_subtitle(path, language, hi, subtitle, provider, providers_a
use_postprocessing = get_general_settings()[10]
postprocessing_cmd = get_general_settings()[11]
if language == 'pb':
language = alpha3_from_alpha2(language)
language = alpha3_from_alpha2(language)
if language == 'pob':
lang_obj = Language('por', 'BR')
else:
language = alpha3_from_alpha2(language)
lang_obj = Language(language)
try:
@ -222,41 +263,44 @@ def manual_download_subtitle(path, language, hi, subtitle, provider, providers_a
logging.exception('Error saving subtitles file to disk.')
return None
else:
downloaded_provider = str(result[0]).strip('<>').split(' ')[0][:-8]
downloaded_language = language_from_alpha3(language)
downloaded_language_code2 = alpha2_from_alpha3(language)
downloaded_language_code3 = language
downloaded_path = get_subtitle_path(path, language=lang_obj)
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(score) + "% using manual search."
if use_postprocessing is True:
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3)
try:
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
if len(result) > 0:
downloaded_provider = result[0].provider_name
downloaded_language = language_from_alpha3(result[0].language.alpha3)
downloaded_language_code2 = alpha2_from_alpha3(result[0].language.alpha3)
downloaded_language_code3 = result[0].language.alpha3
downloaded_path = get_subtitle_path(path, language=lang_obj)
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(score) + "% using manual search."
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
if use_postprocessing is True:
command = pp_replace(postprocessing_cmd, path, downloaded_path, downloaded_language, downloaded_language_code2, downloaded_language_code3)
try:
if os.name == 'nt':
codepage = subprocess.Popen("chcp", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out_codepage, err_codepage = codepage.communicate()
encoding = out_codepage.split(':')[-1].strip()
if os.name == 'nt':
out = out.decode(encoding)
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
out, err = process.communicate()
except:
if out == "":
logging.error('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.error('Post-processing result for file ' + path + ' : ' + out)
else:
if out == "":
logging.info('Post-processing result for file ' + path + ' : Nothing returned from command execution')
if os.name == 'nt':
out = out.decode(encoding)
except:
if out == "":
logging.error('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.error('Post-processing result for file ' + path + ' : ' + out)
else:
logging.info('Post-processing result for file ' + path + ' : ' + out)
if out == "":
logging.info('Post-processing result for file ' + path + ' : Nothing returned from command execution')
else:
logging.info('Post-processing result for file ' + path + ' : ' + out)
return message
return message
else:
return None
def series_download_subtitles(no):
if get_general_settings()[24] is True:
@ -276,7 +320,7 @@ def series_download_subtitles(no):
for episode in episodes_details:
for language in ast.literal_eval(episode[1]):
if language is not None:
message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)), series_details[0], providers_list, providers_auth, episode[3], 'series')
message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)), series_details[0], providers_list, providers_auth, str(episode[3]), 'series')
if message is not None:
store_subtitles(path_replace(episode[0]))
history_log(1, no, episode[2], message)
@ -295,7 +339,7 @@ def movies_download_subtitles(no):
for language in ast.literal_eval(movie[1]):
if language is not None:
message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)), movie[4], providers_list, providers_auth, movie[3], 'movie')
message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)), movie[4], providers_list, providers_auth, str(movie[3]), 'movie')
if message is not None:
store_subtitles_movie(path_replace_movie(movie[0]))
history_log_movie(1, no, message)
@ -334,7 +378,7 @@ def wanted_download_subtitles(path):
for i in range(len(attempt)):
if attempt[i][0] == language:
if search_active(attempt[i][1]) is True:
message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)), episode[4], providers_list, providers_auth, episode[5], 'series')
message = download_subtitle(path_replace(episode[0]), str(alpha3_from_alpha2(language)), episode[4], providers_list, providers_auth, str(episode[5]), 'series')
if message is not None:
store_subtitles(path_replace(episode[0]))
list_missing_subtitles(episode[3])
@ -375,7 +419,7 @@ def wanted_download_subtitles_movie(path):
for i in range(len(attempt)):
if attempt[i][0] == language:
if search_active(attempt[i][1]) is True:
message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)), movie[4], providers_list, providers_auth, movie[5], 'movie')
message = download_subtitle(path_replace_movie(movie[0]), str(alpha3_from_alpha2(language)), movie[4], providers_list, providers_auth, str(movie[5]), 'movie')
if message is not None:
store_subtitles_movie(path_replace_movie(movie[0]))
list_missing_subtitles_movies(movie[3])

@ -5,6 +5,5 @@ 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__

@ -20,12 +20,6 @@ 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
@ -51,7 +45,7 @@ def guess_filename(filename, options):
import yaml
from guessit import yamlutils
ystr = yaml.dump({filename: OrderedDict(guess)}, Dumper=yamlutils.CustomDumper, default_flow_style=False,
ystr = yaml.dump({filename: dict(guess)}, Dumper=yamlutils.CustomDumper, default_flow_style=False,
allow_unicode=True)
i = 0
for yline in ystr.splitlines():

@ -4,4 +4,4 @@
Version module
"""
# pragma: no cover
__version__ = '3.0.0'
__version__ = '2.1.4'

@ -3,13 +3,11 @@
"""
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
@ -17,7 +15,7 @@ import six
from rebulk.introspector import introspect
from .rules import rebulk_builder
from .options import parse_options, load_config
from .options import parse_options
from .__version__ import __version__
@ -43,25 +41,12 @@ class GuessitException(Exception):
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:
:param options: the filename or release name
:type options: str|dict
:return:
:rtype:
@ -73,7 +58,7 @@ def properties(options=None):
"""
Retrieves all properties with possible values that can be guessed
:param options:
:type options: str|dict
:type options:
:return:
:rtype:
"""
@ -85,88 +70,53 @@ class GuessItApi(object):
An api class that can be configured with custom Rebulk configuration.
"""
def __init__(self):
"""Default constructor."""
self.rebulk = None
def __init__(self, rebulk):
"""
:param rebulk: Rebulk instance to use.
:type rebulk: Rebulk
:return:
:rtype:
"""
self.rebulk = rebulk
@classmethod
def _fix_encoding(cls, value):
@staticmethod
def _fix_option_encoding(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()}
return [GuessItApi._fix_option_encoding(item) for item in value]
if six.PY2 and isinstance(value, six.text_type):
return value.encode('utf-8')
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
def guessit(self, string, options=None):
"""
Retrieves all matches from string as a dict
:param string: the filename or release name
:type string: str|Path
:param options:
:type string: str
:param options: the filename or release name
: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)
options = parse_options(options, True)
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)
fixed_options = {}
for (key, value) in options.items():
key = GuessItApi._fix_option_encoding(key)
value = GuessItApi._fix_option_encoding(value)
fixed_options[key] = value
options = fixed_options
if six.PY2 and isinstance(string, six.text_type):
string = string.encode("utf-8")
result_decode = True
if six.PY3 and isinstance(string, six.binary_type):
string = string.decode('ascii')
result_encode = True
matches = self.rebulk.matches(string, options)
if result_decode:
for match in matches:
@ -189,7 +139,6 @@ class GuessItApi(object):
:return:
:rtype:
"""
options = self.configure(options)
unordered = introspect(self.rebulk, options).properties
ordered = OrderedDict()
for k in sorted(unordered.keys(), key=six.text_type):
@ -199,4 +148,4 @@ class GuessItApi(object):
return ordered
default_api = GuessItApi()
default_api = GuessItApi(rebulk_builder())

@ -1,363 +1,5 @@
{
"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"
]
}
}
]
}

@ -4,9 +4,6 @@
JSON Utils
"""
import json
from six import text_type
try:
from collections import OrderedDict
except ImportError: # pragma: no-cover
@ -30,6 +27,6 @@ class GuessitEncoder(json.JSONEncoder):
ret['end'] = o.end
return ret
elif hasattr(o, 'name'): # Babelfish languages/countries long name
return text_type(o.name)
return str(o.name)
else: # pragma: no cover
return text_type(o)
return str(o)

@ -42,10 +42,6 @@ def build_argument_parser():
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,
@ -96,7 +92,7 @@ def parse_options(options=None, api=False):
:param options:
:type options:
:param api
:type api: boolean
:type boolean
:return:
:rtype:
"""
@ -161,12 +157,10 @@ def load_config(options):
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'):
embedded_options_data = pkgutil.get_data('guessit', 'config/options.json').decode("utf-8")
embedded_options = json.loads(embedded_options_data)
configurations.append(embedded_options)
else:
configurations.append({'advanced_config': embedded_options['advanced_config']})
if configurations:
configurations.append(options)

@ -10,7 +10,7 @@ from .markers.groups import groups
from .properties.episodes import episodes
from .properties.container import container
from .properties.source import source
from .properties.format import format_
from .properties.video_codec import video_codec
from .properties.audio_codec import audio_codec
from .properties.screen_size import screen_size
@ -24,7 +24,6 @@ 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
@ -37,50 +36,44 @@ from .properties.type import type_
from .processors import processors
def rebulk_builder(config):
def rebulk_builder():
"""
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')))
rebulk.rebulk(path())
rebulk.rebulk(groups())
rebulk.rebulk(episodes())
rebulk.rebulk(container())
rebulk.rebulk(format_())
rebulk.rebulk(video_codec())
rebulk.rebulk(audio_codec())
rebulk.rebulk(screen_size())
rebulk.rebulk(website())
rebulk.rebulk(date())
rebulk.rebulk(title())
rebulk.rebulk(episode_title())
rebulk.rebulk(language())
rebulk.rebulk(country())
rebulk.rebulk(release_group())
rebulk.rebulk(streaming_service())
rebulk.rebulk(other())
rebulk.rebulk(size())
rebulk.rebulk(edition())
rebulk.rebulk(cds())
rebulk.rebulk(bonus())
rebulk.rebulk(film())
rebulk.rebulk(part())
rebulk.rebulk(crc())
rebulk.rebulk(processors())
rebulk.rebulk(mimetype())
rebulk.rebulk(type_())
def customize_properties(properties):
"""

@ -13,12 +13,9 @@ 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')
)
return not match.private and \
match.name not in ['proper_count', 'title', 'episode_title', 'alternative_title'] and \
not (match.name == 'container' and 'extension' in match.tags)
def marker_weight(matches, marker, predicate):
@ -53,8 +50,9 @@ def marker_comparator(matches, markers, predicate):
matches_count = marker_weight(matches, marker2, predicate) - marker_weight(matches, marker1, predicate)
if matches_count:
return matches_count
# give preference to rightmost path
len_diff = len(marker2) - len(marker1)
if len_diff:
return len_diff
return markers.index(marker2) - markers.index(marker1)
return comparator

@ -42,7 +42,7 @@ def _is_int(string):
return False
def _guess_day_first_parameter(groups): # pylint:disable=inconsistent-return-statements
def _guess_day_first_parameter(groups):
"""
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.
@ -67,7 +67,7 @@ def _guess_day_first_parameter(groups): # pylint:disable=inconsistent-return-st
return True
def search_date(string, year_first=None, day_first=None): # pylint:disable=inconsistent-return-statements
def search_date(string, year_first=None, day_first=None):
"""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

@ -1,27 +0,0 @@
#!/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

@ -1,106 +0,0 @@
#!/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'

@ -32,3 +32,48 @@ def iter_words(string):
i += 1
if inside_word:
yield _Word(span=(last_sep_index+1, i), value=string[last_sep_index+1:i])
# list of common words which could be interpreted as properties, but which
# are far too common to be able to say they represent a property in the
# middle of a string (where they most likely carry their commmon meaning)
COMMON_WORDS = frozenset([
# english words
'is', 'it', 'am', 'mad', 'men', 'man', 'run', 'sin', 'st', 'to',
'no', 'non', 'war', 'min', 'new', 'car', 'day', 'bad', 'bat', 'fan',
'fry', 'cop', 'zen', 'gay', 'fat', 'one', 'cherokee', 'got', 'an', 'as',
'cat', 'her', 'be', 'hat', 'sun', 'may', 'my', 'mr', 'rum', 'pi', 'bb',
'bt', 'tv', 'aw', 'by', 'md', 'mp', 'cd', 'lt', 'gt', 'in', 'ad', 'ice',
'ay', 'at', 'star', 'so', 'he', 'do', 'ax', 'mx',
# french words
'bas', 'de', 'le', 'son', 'ne', 'ca', 'ce', 'et', 'que',
'mal', 'est', 'vol', 'or', 'mon', 'se', 'je', 'tu', 'me',
'ne', 'ma', 'va', 'au', 'lu',
# japanese words,
'wa', 'ga', 'ao',
# spanish words
'la', 'el', 'del', 'por', 'mar', 'al',
# italian words
'un',
# other
'ind', 'arw', 'ts', 'ii', 'bin', 'chan', 'ss', 'san', 'oss', 'iii',
'vi', 'ben', 'da', 'lt', 'ch', 'sr', 'ps', 'cx', 'vo',
# new from babelfish
'mkv', 'avi', 'dmd', 'the', 'dis', 'cut', 'stv', 'des', 'dia', 'and',
'cab', 'sub', 'mia', 'rim', 'las', 'une', 'par', 'srt', 'ano', 'toy',
'job', 'gag', 'reel', 'www', 'for', 'ayu', 'csi', 'ren', 'moi', 'sur',
'fer', 'fun', 'two', 'big', 'psy', 'air',
# movie title
'brazil', 'jordan',
# release groups
'bs', # Bosnian
'kz',
# countries
'gt', 'lt', 'im',
# part/pt
'pt',
# screener
'scr',
# quality
'sd', 'hr'
])

@ -6,20 +6,17 @@ Groups markers (...), [...] and {...}
from rebulk import Rebulk
def groups(config):
def groups():
"""
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']
starting = '([{'
ending = ')]}'
def mark_groups(input_string):
"""

@ -8,12 +8,9 @@ from rebulk import Rebulk
from rebulk.utils import find_all
def path(config): # pylint:disable=unused-argument
def path():
"""
Builder for rebulk object.
:param config: rule configuration
:type config: dict
:return: Created Rebulk object
:rtype: Rebulk
"""
@ -25,7 +22,6 @@ def path(config): # pylint:disable=unused-argument
Functional pattern to mark path elements.
:param input_string:
:param context:
:return:
"""
ret = []

@ -34,7 +34,8 @@ class EnlargeGroupMatches(CustomRule):
for match in matches.ending(group.end - 1):
ending.append(match)
return starting, ending
if starting or ending:
return starting, ending
def then(self, matches, when_response, context):
starting, ending = when_response
@ -225,12 +226,9 @@ class StripSeparators(CustomRule):
match.raw_end -= 1
def processors(config): # pylint:disable=unused-argument
def processors():
"""
Builder for rebulk object.
:param config: rule configuration
:type config: dict
:return: Created Rebulk object
:rtype: Rebulk
"""

@ -6,20 +6,15 @@ 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
def audio_codec():
"""
Builder for rebulk object.
:param config: rule configuration
:type config: dict
:return: Created Rebulk object
:rtype: Rebulk
"""
@ -41,38 +36,28 @@ def audio_codec(config): # pylint:disable=unused-argument
return match1
return '__default__'
rebulk.defaults(name='audio_codec',
conflict_solver=audio_codec_priority,
disabled=lambda context: is_disabled(context, 'audio_codec'))
rebulk.defaults(name="audio_codec", conflict_solver=audio_codec_priority)
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.regex('Dolby', 'DolbyDigital', 'Dolby-Digital', 'DD', 'AC3D?', value='AC3')
rebulk.regex("DolbyAtmos", "Dolby-Atmos", "Atmos", value="DolbyAtmos")
rebulk.string("AAC", value="AAC")
rebulk.string('EAC3', 'DDP', 'DD+', value='Dolby Digital Plus')
rebulk.string('EAC3', 'DDP', 'DD+', value="EAC3")
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("True-?HD", value="TrueHD")
rebulk.defaults(name="audio_profile")
rebulk.string("HD", value="HD", tags="DTS")
rebulk.regex("HD-?MA", value="HDMA", tags="DTS")
rebulk.string("HE", value="HE", tags="AAC")
rebulk.string("LC", value="LC", tags="AAC")
rebulk.string("HQ", value="HQ", tags="AC3")
rebulk.defaults(name="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')
@ -81,7 +66,7 @@ def audio_codec(config): # pylint:disable=unused-argument
rebulk.string('2ch', 'stereo', value='2.0')
rebulk.string('1ch', 'mono', value='1.0')
rebulk.rules(DtsHDRule, AacRule, DolbyDigitalRule, AudioValidatorRule, HqConflictRule, AudioChannelsValidatorRule)
rebulk.rules(DtsRule, AacRule, Ac3Rule, AudioValidatorRule, HqConflictRule, AudioChannelsValidatorRule)
return rebulk
@ -126,9 +111,6 @@ class AudioProfileRule(Rule):
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 = []
@ -138,18 +120,16 @@ class AudioProfileRule(Rule):
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):
class DtsRule(AudioProfileRule):
"""
Rule to validate DTS-HD profile
Rule to validate DTS profile
"""
def __init__(self):
super(DtsHDRule, self).__init__('DTS-HD')
super(DtsRule, self).__init__("DTS")
class AacRule(AudioProfileRule):
@ -161,13 +141,13 @@ class AacRule(AudioProfileRule):
super(AacRule, self).__init__("AAC")
class DolbyDigitalRule(AudioProfileRule):
class Ac3Rule(AudioProfileRule):
"""
Rule to validate Dolby Digital profile
Rule to validate AC3 profile
"""
def __init__(self):
super(DolbyDigitalRule, self).__init__('Dolby Digital')
super(Ac3Rule, self).__init__("AC3")
class HqConflictRule(Rule):
@ -175,16 +155,16 @@ class HqConflictRule(Rule):
Solve conflict between HQ from other property and from audio_profile.
"""
dependency = [DtsHDRule, AacRule, DolbyDigitalRule]
dependency = [DtsRule, AacRule, Ac3Rule]
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 = matches.named('audio_profile', lambda match: match.value == 'HQ')
hq_audio_spans = [match.span for match in hq_audio]
return matches.named('other', lambda m: m.span in hq_audio_spans)
hq_other = matches.named('other', lambda match: match.span in hq_audio_spans)
if hq_other:
return hq_other
class AudioChannelsValidatorRule(Rule):
@ -194,9 +174,6 @@ class AudioChannelsValidatorRule(Rule):
priority = 128
consequence = RemoveMatch
def enabled(self, context):
return not is_disabled(context, 'audio_channels')
def when(self, matches, context):
ret = []

@ -1,72 +0,0 @@
#!/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

@ -9,26 +9,21 @@ 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
def bonus():
"""
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 = 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
if conflicting.name in ['video_codec', 'episode'] and 'bonus-conflict' not in conflicting.tags
else '__default__')
rebulk.rules(BonusTitleRule)
@ -45,7 +40,7 @@ class BonusTitleRule(Rule):
properties = {'bonus_title': [None]}
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
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)

@ -6,22 +6,16 @@ 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
def cds():
"""
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 = 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,

@ -8,35 +8,33 @@ 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):
def container():
"""
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 = 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
if other.name in ['format', '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']
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']
rebulk.regex(r'\.'+build_or_pattern(subtitles)+'$', exts=subtitles, tags=['extension', 'subtitle'])
rebulk.regex(r'\.'+build_or_pattern(info)+'$', exts=info, tags=['extension', 'info'])
@ -48,11 +46,11 @@ def container(config):
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
if other.name in ['format',
'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(*[sub for sub in subtitles if sub not in ['sub']], tags=['subtitle'])
rebulk.string(*videos, tags=['video'])
rebulk.string(*torrent, tags=['torrent'])
rebulk.string(*nzb, tags=['nzb'])

@ -7,50 +7,41 @@ country property
import babelfish
from rebulk import Rebulk
from ..common.pattern import is_disabled
from ..common.words import iter_words
from ..common.words import COMMON_WORDS, iter_words
def country(config, common_words):
def country():
"""
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 = Rebulk().defaults(name='country')
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'))
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'])
properties={'country': [None]})
return rebulk
COUNTRIES_SYN = {'ES': ['españa'],
'GB': ['UK'],
'BR': ['brazilian', 'bra'],
'CA': ['québec', 'quebec', 'qc'],
# FIXME: this one is a bit of a stretch, not sure how to do it properly, though...
'MX': ['Latinoamérica', 'latin america']}
class GuessitCountryConverter(babelfish.CountryReverseConverter): # pylint: disable=missing-docstring
def __init__(self, synonyms):
def __init__(self):
self.guessit_exceptions = {}
for alpha2, synlist in synonyms.items():
for alpha2, synlist in COUNTRIES_SYN.items():
for syn in synlist:
self.guessit_exceptions[syn.lower()] = alpha2
@ -87,28 +78,32 @@ class GuessitCountryConverter(babelfish.CountryReverseConverter): # pylint: dis
raise babelfish.CountryReverseError(name)
class CountryFinder(object):
"""Helper class to search and return country matches."""
babelfish.country_converters['guessit'] = GuessitCountryConverter()
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 is_allowed_country(country_object, context=None):
"""
Check if country is allowed.
"""
if context and context.get('allowed_countries'):
allowed_countries = context.get('allowed_countries')
return country_object.name.lower() in allowed_countries or country_object.alpha2.lower() in allowed_countries
return True
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}
def find_countries(string, context=None):
"""
Find countries in given string.
"""
ret = []
for word_match in iter_words(string.strip().lower()):
word = word_match.value
if word.lower() in COMMON_WORDS:
continue
try:
country_object = babelfish.Country.fromguessit(word)
if is_allowed_country(country_object, context):
ret.append((word_match.span[0], word_match.span[1], {'value': country_object}))
except babelfish.Error:
continue
return ret

@ -6,21 +6,16 @@ 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
def crc():
"""
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 = Rebulk().regex_defaults(flags=re.IGNORECASE)
rebulk.defaults(validator=seps_surround)
rebulk.regex('(?:[a-fA-F]|[0-9]){8}', name='crc32',

@ -6,26 +6,21 @@ 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
def date():
"""
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
def date_functional(string, context):
"""
Search for date in the string and retrieves match
@ -38,9 +33,8 @@ def date(config): # pylint:disable=unused-argument
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')
if other.name in ['episode', 'season']
else '__default__')
rebulk.rules(KeepMarkedYearInFilepart)
@ -55,9 +49,6 @@ class KeepMarkedYearInFilepart(Rule):
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:

@ -7,34 +7,28 @@ 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
def edition():
"""
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 = 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',
rebulk.regex('collector', 'collector-edition', 'edition-collector', value='Collector Edition')
rebulk.regex('special-edition', 'edition-special', value='Special Edition',
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.string('se', value='Special Edition', tags='has-neighbor')
rebulk.regex('criterion-edition', 'edition-criterion', value='Criterion Edition')
rebulk.regex('deluxe', 'deluxe-edition', 'edition-deluxe', value='Deluxe Edition')
rebulk.regex('limited', 'limited-edition', value='Limited Edition', tags=['has-neighbor', 'release-group-prefix'])
rebulk.regex(r'theatrical-cut', r'theatrical-edition', r'theatrical', value='Theatrical Edition')
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',
@ -43,10 +37,5 @@ def edition(config): # pylint:disable=unused-argument
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

@ -9,31 +9,26 @@ from rebulk import Rebulk, Rule, AppendMatch, RemoveMatch, RenameMatch, POST_PRO
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
def episode_title():
"""
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)
rebulk = Rebulk().rules(RemoveConflictsWithEpisodeTitle(previous_names),
EpisodeTitleFromPosition(previous_names),
AlternativeTitleReplace(previous_names),
TitleToEpisodeTitle,
Filepart3EpisodeTitle,
Filepart2EpisodeTitle,
RenameEpisodeTitleWhenMovieType)
return rebulk
@ -48,7 +43,7 @@ class RemoveConflictsWithEpisodeTitle(Rule):
def __init__(self, previous_names):
super(RemoveConflictsWithEpisodeTitle, self).__init__()
self.previous_names = previous_names
self.next_names = ('streaming_service', 'screen_size', 'source',
self.next_names = ('streaming_service', 'screen_size', 'format',
'video_codec', 'audio_codec', 'other', 'container')
self.affected_if_holes_after = ('part', )
self.affected_names = ('part', 'year')
@ -58,11 +53,13 @@ class RemoveConflictsWithEpisodeTitle(Rule):
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)
before = matches.previous(match, index=0,
predicate=lambda m, fp=filepart: not m.private and m.start >= fp.start)
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)
after = matches.next(match, index=0,
predicate=lambda m, fp=filepart: not m.private and m.end <= fp.end)
if not after or after.name not in self.next_names:
continue
@ -103,15 +100,16 @@ class TitleToEpisodeTitle(Rule):
for title in titles:
title_groups[title.value].append(title)
episode_titles = []
if len(title_groups) < 2:
return episode_titles
return
episode_titles = []
for title in titles:
if matches.previous(title, lambda match: match.name == 'episode'):
episode_titles.append(title)
return episode_titles
if episode_titles:
return episode_titles
def then(self, matches, when_response, context):
for title in when_response:
@ -152,7 +150,7 @@ class EpisodeTitleFromPosition(TitleBaseRule):
return False
return super(EpisodeTitleFromPosition, self).should_remove(match, matches, filepart, hole, context)
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
if matches.named('episode_title'):
return
return super(EpisodeTitleFromPosition, self).when(matches, context)
@ -169,7 +167,7 @@ class AlternativeTitleReplace(Rule):
super(AlternativeTitleReplace, self).__init__()
self.previous_names = previous_names
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
if matches.named('episode_title'):
return
@ -204,7 +202,7 @@ class RenameEpisodeTitleWhenMovieType(Rule):
dependency = TypeProcessor
consequence = RenameMatch
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
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')
@ -228,7 +226,7 @@ class Filepart3EpisodeTitle(Rule):
"""
consequence = AppendMatch('title')
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
fileparts = matches.markers.named('path')
if len(fileparts) < 3:
return
@ -269,7 +267,7 @@ class Filepart2EpisodeTitle(Rule):
"""
consequence = AppendMatch('title')
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
fileparts = matches.markers.named('path')
if len(fileparts) < 2:
return

@ -1,7 +1,7 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
episode, season, disc, episode_count, season_count and episode_details properties
episode, season, episode_count, season_count and episode_details properties
"""
import copy
from collections import defaultdict
@ -12,34 +12,24 @@ 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 import dash, alt_dash, seps
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):
def episodes():
"""
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 = 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.
@ -49,11 +39,11 @@ def episodes(config):
:rtype:
"""
eps = matches.named('episode')
if len(eps) > 1 and abs(eps[-1].value - eps[-2].value) > episode_max_range:
if len(eps) > 1 and abs(eps[-1].value - eps[-2].value) > 100:
return True
seasons = matches.named('season')
if len(seasons) > 1 and abs(seasons[-1].value - seasons[-2].value) > season_max_range:
if len(seasons) > 1 and abs(seasons[-1].value - seasons[-2].value) > 100:
return True
return False
@ -67,41 +57,40 @@ def episodes(config):
:param other:
:return:
"""
if match.name != other.name:
if match.name == 'episode' and other.name == 'year':
if match.name == 'episode' and other.name in \
['screen_size', 'video_codec', 'audio_codec', 'audio_channels', 'container', 'date', 'year'] \
and 'weak-audio_channels' not in other.tags:
return match
if match.name == 'season' and other.name in \
['screen_size', 'video_codec', 'audio_codec', 'audio_channels', 'container', 'date'] \
and 'weak-audio_channels' not in other.tags:
return match
if match.name in ['season', 'episode'] and other.name in ['season', 'episode'] \
and match.initiator != other.initiator:
if 'weak-episode' in match.tags or 'x' in match.initiator.raw.lower():
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
if 'weak-episode' in other.tags or 'x' in other.initiator.raw.lower():
return other
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']
season_episode_seps = []
season_episode_seps.extend(seps)
season_episode_seps.extend(['x', 'X', 'e', 'E'])
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']
season_markers = ["S"]
season_ep_markers = ["x"]
episode_markers = ["xE", "Ex", "EP", "E", "x"]
range_separators = ['-', '~', 'to', 'a']
weak_discrete_separators = list(sep for sep in seps if sep not in range_separators)
strong_discrete_separators = ['+', '&', 'and', 'et']
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.
@ -136,7 +125,7 @@ def episodes(config):
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:
if not current_match.value - previous_match.value == 1:
valid = False
if separator.raw in strong_discrete_separators:
valid = True
@ -154,13 +143,12 @@ def episodes(config):
private_parent=True,
validate_all=True,
validator={'__parent__': ordering_validator},
conflict_solver=season_episode_conflict_solver,
disabled=is_season_episode_disabled) \
conflict_solver=season_episode_conflict_solver) \
.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+)',
build_or_pattern(episode_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,
.regex(build_or_pattern(episode_markers + discrete_separators + range_separators,
name='episodeSeparator',
escape=True) +
r'(?P<episode>\d+)').repeater('*') \
@ -190,11 +178,9 @@ def episodes(config):
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'))
for episode_detail in ('Special', 'Bonus', 'Omake', 'Ova', 'Oav', 'Pilot', 'Unaired'):
rebulk.string(episode_detail, value=episode_detail, name='episode_details')
rebulk.regex(r'Extras?', name='episode_details', value='Extras')
def validate_roman(match):
"""
@ -216,8 +202,7 @@ def episodes(config):
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')) \
'count': validate_roman}) \
.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('?') \
@ -229,7 +214,7 @@ def episodes(config):
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'))
disabled=lambda context: context.get('type') == 'episode')
rebulk.regex(build_or_pattern(episode_words, name='episodeMarker') + r'-?(?P<episode>' + numeral + ')' +
r'(?:v(?P<version>\d+))?' +
@ -237,44 +222,42 @@ def episodes(config):
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'))
disabled=lambda context: context.get('type') != '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'))
formatter={'season': int, 'other': lambda match: 'Complete'})
# 12, 13
rebulk.chain(tags=['weak-episode'], formatter={'episode': int, 'version': int},
disabled=lambda context: context.get('type') == 'movie' or is_disabled(context, 'episode')) \
rebulk.chain(tags=['bonus-conflict', 'weak-movie', 'weak-episode'], formatter={'episode': int, 'version': int},
disabled=lambda context: context.get('type') == 'movie') \
.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')) \
rebulk.chain(tags=['bonus-conflict', 'weak-movie', 'weak-episode'], formatter={'episode': int, 'version': int},
disabled=lambda context: context.get('type') == 'movie') \
.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')) \
rebulk.chain(tags=['bonus-conflict', 'weak-movie', 'weak-episode'], formatter={'episode': int, 'version': int},
disabled=lambda context: (not context.get('episode_prefer_number', False) or
context.get('type') == 'movie')) \
.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')) \
rebulk.chain(tags=['bonus-conflict', 'weak-movie', 'weak-episode'], formatter={'episode': int, 'version': int},
disabled=lambda context: context.get('type') != 'episode') \
.defaults(validator=None) \
.regex(r'(?P<episode>\d)') \
.regex(r'v(?P<version>\d+)').repeater('?') \
@ -282,16 +265,14 @@ def episodes(config):
# 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')) \
rebulk.chain(formatter={'episode': int, 'version': int}) \
.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')) \
rebulk.chain(abbreviations=[dash], formatter={'episode': int, 'version': int}) \
.defaults(validator=None) \
.regex(r'ep-?(?P<episode>\d{1,4})') \
.regex(r'v(?P<version>\d+)').repeater('?') \
@ -300,26 +281,23 @@ def episodes(config):
# cap 112, cap 112_114
rebulk.chain(abbreviations=[dash],
tags=['see-pattern'],
formatter={'season': int, 'episode': int},
disabled=is_season_episode_disabled) \
formatter={'season': int, 'episode': int}) \
.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'],
rebulk.chain(tags=['bonus-conflict', 'weak-movie', 'weak-episode', 'weak-duplicate'],
formatter={'season': int, 'episode': int, 'version': int},
name='weak_duplicate',
conflict_solver=season_episode_conflict_solver,
conflict_solver=lambda match, other: match if other.name == 'year' else '__default__',
disabled=lambda context: (context.get('episode_prefer_number', False) or
context.get('type') == 'movie') or is_season_episode_disabled(context)) \
context.get('type') == 'movie')) \
.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.regex(r'v(?P<version>\d+)', children=True, private_parent=True, formatter=int)
rebulk.defaults(private_names=['episodeSeparator', 'seasonSeparator'])
@ -327,100 +305,19 @@ def episodes(config):
# 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'))
abbreviations=[dash], children=True, private_parent=True, formatter=int)
rebulk.regex(r'Minisodes?', name='episode_format', value="Minisode",
disabled=lambda context: is_disabled(context, 'episode_format'))
rebulk.regex(r'Minisodes?', name='episode_format', value="Minisode")
rebulk.rules(WeakConflictSolver, RemoveInvalidSeason, RemoveInvalidEpisode,
SeePatternRange(range_separators + ['_']),
EpisodeNumberSeparatorRange(range_separators),
rebulk.rules(RemoveInvalidSeason, RemoveInvalidEpisode,
SeePatternRange(range_separators + ['_']), EpisodeNumberSeparatorRange(range_separators),
SeasonSeparatorRange(range_separators), RemoveWeakIfMovie, RemoveWeakIfSxxExx,
RemoveWeakDuplicate, EpisodeDetailValidator, RemoveDetachedEpisodeNumber, VersionValidator,
RemoveWeak, RenameToAbsoluteEpisode, CountValidator, EpisodeSingleDigitValidator, RenameToDiscMatch)
CountValidator, EpisodeSingleDigitValidator)
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
@ -499,16 +396,14 @@ class AbstractSeparatorRange(Rule):
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
previous_match = matches.previous(separator, lambda match: match.name == self.property_name, 0)
next_match = matches.next(separator, lambda match: match.name == self.property_name, 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):
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)
@ -520,11 +415,9 @@ class AbstractSeparatorRange(Rule):
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',
@ -538,46 +431,12 @@ class AbstractSeparatorRange(Rule):
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.
"""
priority = 128
consequence = [RemoveMatch, AppendMatch]
def __init__(self, range_separators):
super(EpisodeNumberSeparatorRange, self).__init__(range_separators, "episode")
@ -587,6 +446,8 @@ class SeasonSeparatorRange(AbstractSeparatorRange):
"""
Remove separator matches and create matches for season range.
"""
priority = 128
consequence = [RemoveMatch, AppendMatch]
def __init__(self, range_separators):
super(SeasonSeparatorRange, self).__init__(range_separators, "season")
@ -594,7 +455,7 @@ class SeasonSeparatorRange(AbstractSeparatorRange):
class RemoveWeakIfMovie(Rule):
"""
Remove weak-episode tagged matches if it seems to be a movie.
Remove weak-movie tagged matches if it seems to be a movie.
"""
priority = 64
consequence = RemoveMatch
@ -610,48 +471,19 @@ class RemoveWeakIfMovie(Rule):
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')):
next_match = matches.next(year, predicate=lambda m, fp=filepart: m.private and m.end <= fp.end, index=0)
if next_match 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)))
to_remove.extend(matches.tagged('weak-movie', predicate=lambda m: m.initiator not in to_ignore))
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.
Remove weak-movie tagged matches if SxxExx pattern is matched.
"""
priority = 64
consequence = RemoveMatch
@ -660,10 +492,9 @@ class RemoveWeakIfSxxExx(Rule):
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)
predicate=lambda match: not match.private and 'SxxExx' in match.tags):
to_remove.extend(matches.range(
filepart.start, filepart.end, predicate=lambda match: 'weak-movie' in match.tags))
return to_remove
@ -744,7 +575,7 @@ class RemoveWeakDuplicate(Rule):
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)):
predicate=lambda match: 'weak-duplicate' in match.tags)):
if match.pattern in patterns[match.name]:
to_remove.append(match)
else:
@ -784,12 +615,12 @@ class RemoveDetachedEpisodeNumber(Rule):
episode_numbers = []
episode_values = set()
for match in matches.named('episode', lambda m: not m.private and 'weak-episode' in m.tags):
for match in matches.named('episode', lambda match: not match.private and 'weak-movie' in match.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))
episode_numbers = list(sorted(episode_numbers, key=lambda match: match.value))
if len(episode_numbers) > 1 and \
episode_numbers[0].value < 10 and \
episode_numbers[1].value - episode_numbers[0].value != 1:
@ -833,29 +664,3 @@ class EpisodeSingleDigitValidator(Rule):
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

@ -7,11 +7,10 @@ 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
def film():
"""
Builder for rebulk object.
:return: Created Rebulk object
@ -19,8 +18,7 @@ def film(config): # pylint:disable=unused-argument
"""
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.regex(r'f(\d{1,2})', name='film', private_parent=True, children=True, formatter=int)
rebulk.rules(FilmTitleRule)
@ -35,10 +33,7 @@ class FilmTitleRule(Rule):
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
def when(self, matches, context):
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)

@ -0,0 +1,72 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
format property
"""
from rebulk.remodule import re
from rebulk import Rebulk, RemoveMatch, Rule
from ..common import dash
from ..common.validators import seps_before, seps_after
def format_():
"""
Builder for rebulk object.
:return: Created Rebulk object
:rtype: Rebulk
"""
rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, abbreviations=[dash])
rebulk.defaults(name="format", tags=['video-codec-prefix', 'streaming_service.suffix'])
rebulk.regex("VHS", "VHS-?Rip", value="VHS")
rebulk.regex("CAM", "CAM-?Rip", "HD-?CAM", value="Cam")
rebulk.regex("TELESYNC", "TS", "HD-?TS", value="Telesync")
rebulk.regex("WORKPRINT", "WP", value="Workprint")
rebulk.regex("TELECINE", "TC", value="Telecine")
rebulk.regex("PPV", "PPV-?Rip", value="PPV") # Pay Per View
rebulk.regex("SD-?TV", "SD-?TV-?Rip", "Rip-?SD-?TV", "TV-?Rip",
"Rip-?TV", "TV-?(?=Dub)", value="TV") # TV is too common to allow matching
rebulk.regex("DVB-?Rip", "DVB", "PD-?TV", value="DVB")
rebulk.regex("DVD", "DVD-?Rip", "VIDEO-?TS", "DVD-?R(?:$|(?!E))", # "DVD-?R(?:$|^E)" => DVD-Real ...
"DVD-?9", "DVD-?5", value="DVD")
rebulk.regex("HD-?TV", "TV-?RIP-?HD", "HD-?TV-?RIP", "HD-?RIP", value="HDTV",
conflict_solver=lambda match, other: other if other.name == 'other' else '__default__')
rebulk.regex("VOD", "VOD-?Rip", value="VOD")
rebulk.regex("WEB-?Rip", "WEB-?DL-?Rip", "WEB-?Cap", value="WEBRip")
rebulk.regex("WEB-?DL", "WEB-?HD", "WEB", "DL-?WEB", "DL(?=-?Mux)", value="WEB-DL")
rebulk.regex("HD-?DVD-?Rip", "HD-?DVD", value="HD-DVD")
rebulk.regex("Blu-?ray(?:-?Rip)?", "B[DR]", "B[DR]-?Rip", "BD[59]", "BD25", "BD50", value="BluRay")
rebulk.regex("AHDTV", value="AHDTV")
rebulk.regex('UHD-?TV', 'UHD-?Rip', value='UHDTV',
conflict_solver=lambda match, other: other if other.name == 'other' else '__default__')
rebulk.regex("HDTC", value="HDTC")
rebulk.regex("DSR", "DSR?-?Rip", "SAT-?Rip", "DTH", "DTH-?Rip", value="SATRip")
rebulk.rules(ValidateFormat)
return rebulk
class ValidateFormat(Rule):
"""
Validate format with screener property, with video_codec property or separated
"""
priority = 64
consequence = RemoveMatch
def when(self, matches, context):
ret = []
for format_match in matches.named('format'):
if not seps_before(format_match) and \
not matches.range(format_match.start - 1, format_match.start - 2,
lambda match: 'format-prefix' in match.tags):
ret.append(format_match)
continue
if not seps_after(format_match) and \
not matches.range(format_match.end, format_match.end + 1,
lambda match: 'format-suffix' in match.tags):
ret.append(format_match)
continue
return ret

@ -11,80 +11,55 @@ 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.words import iter_words, COMMON_WORDS
from ..common.validators import seps_surround
def language(config, common_words):
def language():
"""
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 = Rebulk()
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'))
validator=seps_surround, tags=['release-group-prefix'])
rebulk.string(*subtitle_suffixes, name="subtitle_language.suffix", ignore_case=True, private=True,
validator=seps_surround,
disabled=lambda context: is_disabled(context, 'subtitle_language'))
validator=seps_surround)
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'])
validator=seps_surround, tags=['format-suffix'])
rebulk.functional(find_languages, properties={'language': [None]})
rebulk.rules(SubtitlePrefixLanguageRule, SubtitleSuffixLanguageRule, SubtitleExtensionRule)
return rebulk
COMMON_WORDS_STRICT = frozenset(['brazil'])
UNDETERMINED = babelfish.Language('und')
SYN = {('ell', None): ['gr', 'greek'],
('spa', None): ['esp', 'español', 'espanol'],
('fra', None): ['français', 'vf', 'vff', 'vfi', 'vfq'],
('swe', None): ['se'],
('por', 'BR'): ['po', 'pb', 'pob', 'ptbr', 'br', 'brazilian'],
('cat', None): ['català', 'castellano', 'espanol castellano', 'español castellano'],
('ces', None): ['cz'],
('ukr', None): ['ua'],
('zho', None): ['cn'],
('jpn', None): ['jp'],
('hrv', None): ['scr'],
('mul', None): ['multi', 'dl']} # http://scenelingo.wordpress.com/2009/03/24/what-does-dl-mean/
class GuessitConverter(babelfish.LanguageReverseConverter): # pylint: disable=missing-docstring
_with_country_regexp = re.compile(r'(.*)\((.*)\)')
_with_country_regexp2 = re.compile(r'(.*)-(.*)')
def __init__(self, synonyms):
def __init__(self):
self.guessit_exceptions = {}
for code, synlist in synonyms.items():
if '_' in code:
(alpha3, country) = code.split('_')
else:
(alpha3, country) = (code, None)
for (alpha3, country), synlist in SYN.items():
for syn in synlist:
self.guessit_exceptions[syn.lower()] = (alpha3, country, None)
@ -101,7 +76,15 @@ class GuessitConverter(babelfish.LanguageReverseConverter): # pylint: disable=m
return str(babelfish.Language(alpha3, country, script))
def reverse(self, name): # pylint:disable=arguments-differ
with_country = (GuessitConverter._with_country_regexp.match(name) or
GuessitConverter._with_country_regexp2.match(name))
name = name.lower()
if with_country:
lang = babelfish.Language.fromguessit(with_country.group(1).strip())
lang.country = babelfish.Country.fromguessit(with_country.group(2).strip())
return lang.alpha3, lang.country.alpha2 if lang.country else None, lang.script or None
# exceptions come first, as they need to override a potential match
# with any of the other guessers
try:
@ -113,8 +96,7 @@ class GuessitConverter(babelfish.LanguageReverseConverter): # pylint: disable=m
babelfish.Language.fromalpha3b,
babelfish.Language.fromalpha2,
babelfish.Language.fromname,
babelfish.Language.fromopensubtitles,
babelfish.Language.fromietf]:
babelfish.Language.fromopensubtitles]:
try:
reverse = conv(name)
return reverse.alpha3, reverse.country, reverse.script
@ -131,6 +113,24 @@ def length_comparator(value):
return len(value)
babelfish.language_converters['guessit'] = GuessitConverter()
subtitle_both = ['sub', 'subs', 'subbed', 'custom subbed', 'custom subs',
'custom sub', 'customsubbed', 'customsubs', 'customsub',
'soft subtitles', 'soft subs']
subtitle_prefixes = sorted(subtitle_both +
['st', 'vost', 'subforced', 'fansub', 'hardsub',
'legenda', 'legendas', 'legendado', 'subtitulado',
'soft', 'subtitles'], key=length_comparator)
subtitle_suffixes = sorted(subtitle_both +
['subforced', 'fansub', 'hardsub'], key=length_comparator)
lang_both = ['dublado', 'dubbed', 'dub']
lang_suffixes = sorted(lang_both + ['audio'], key=length_comparator)
lang_prefixes = sorted(lang_both + ['true'], key=length_comparator)
weak_prefixes = ('audio', 'true')
_LanguageMatch = namedtuple('_LanguageMatch', ['property_name', 'word', 'lang'])
@ -149,7 +149,7 @@ class LanguageWord(object):
self.next_word = next_word
@property
def extended_word(self): # pylint:disable=inconsistent-return-statements
def extended_word(self):
"""
Return the extended word for this instance, if any.
"""
@ -175,17 +175,10 @@ def to_rebulk_match(language_match):
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, dict(name=name, value=word.value.lower(),
formatter=babelfish.Language, tags=['weak-language'])
return start, end, {
'name': name,
'value': language_match.lang
}
return start, end, dict(name=name, value=language_match.lang)
class LanguageFinder(object):
@ -193,21 +186,10 @@ 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 __init__(self, allowed_languages):
self.parsed = dict()
self.allowed_languages = allowed_languages
self.common_words = COMMON_WORDS_STRICT if allowed_languages else COMMON_WORDS
def find(self, string):
"""
@ -268,11 +250,11 @@ class LanguageFinder(object):
"""
tuples = [
(language_word, language_word.next_word,
self.prefixes_map,
dict(subtitle_language=subtitle_prefixes, language=lang_prefixes),
lambda string, prefix: string.startswith(prefix),
lambda string, prefix: string[len(prefix):]),
(language_word.next_word, language_word,
self.suffixes_map,
dict(subtitle_language=subtitle_suffixes, language=lang_suffixes),
lambda string, suffix: string.endswith(suffix),
lambda string, suffix: string[:len(string) - len(suffix)])
]
@ -289,7 +271,7 @@ class LanguageFinder(object):
if match:
yield match
def find_match_for_word(self, word, fallback_word, affixes, is_affix, strip_affix): # pylint:disable=inconsistent-return-statements
def find_match_for_word(self, word, fallback_word, affixes, is_affix, strip_affix):
"""
Return the language match for the given word and affixes.
"""
@ -298,6 +280,8 @@ class LanguageFinder(object):
continue
word_lang = current_word.value.lower()
if word_lang in self.common_words:
continue
for key, parts in affixes.items():
for part in parts:
@ -307,31 +291,30 @@ class LanguageFinder(object):
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 fallback_word:
match = self.find_language_match_for_word(fallback_word, key=key, force=True)
if not match and part not in self.weak_affixes:
if not match and part not in weak_prefixes:
match = self.create_language_match(key, LanguageWord(current_word.start, current_word.end,
'und', current_word.input_string))
else:
elif value not in self.common_words:
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
def find_language_match_for_word(self, word, key='language', force=False):
"""
Return the language match for the given word.
"""
for current_word in (word.extended_word, word):
if current_word:
if current_word and (force or current_word.value.lower() not in self.common_words):
match = self.create_language_match(key, current_word)
if match:
return match
def create_language_match(self, key, word): # pylint:disable=inconsistent-return-statements
def create_language_match(self, key, word):
"""
Create a LanguageMatch for a given word
"""
@ -340,21 +323,40 @@ class LanguageFinder(object):
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
def parse_language(self, lang_word):
"""
Parse the lang_word into a valid Language.
Multi and Undetermined languages are also valid languages.
"""
if lang_word in self.parsed:
return self.parsed[lang_word]
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):
if self.allowed_languages:
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:
self.parsed[lang_word] = lang
return lang
# Keep language with alpha2 equivalent. Others are probably
# uncommon languages.
elif lang in ('mul', UNDETERMINED) or hasattr(lang, 'alpha2'):
self.parsed[lang_word] = lang
return lang
self.parsed[lang_word] = None
except babelfish.Error:
pass
self.parsed[lang_word] = None
def find_languages(string, context=None):
"""Find languages in the string
:return: list of tuple (property, Language, lang_word, word)
"""
return LanguageFinder(context.get('allowed_languages')).find(string)
class SubtitlePrefixLanguageRule(Rule):
@ -365,9 +367,6 @@ class SubtitlePrefixLanguageRule(Rule):
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')
@ -413,9 +412,6 @@ class SubtitleSuffixLanguageRule(Rule):
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')
@ -440,64 +436,17 @@ 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.
Since it's a strong match, it also removes any conflicting format 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
def when(self, matches, context):
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
return matches.conflicting(subtitle_lang, lambda m: m.name == 'format'), subtitle_lang

@ -8,23 +8,16 @@ 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
def mimetype():
"""
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
return Rebulk().rules(Mimetype)
class Mimetype(CustomRule):

@ -5,43 +5,38 @@ other property
"""
import copy
from rebulk import Rebulk, Rule, RemoveMatch, RenameMatch, POST_PROCESS, AppendMatch
from rebulk import Rebulk, Rule, RemoveMatch, 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
def other():
"""
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 = 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.regex('Audio-?Fix', 'Audio-?Fixed', value='AudioFix')
rebulk.regex('Sync-?Fix', 'Sync-?Fixed', value='SyncFix')
rebulk.regex('Dual', 'Dual-?Audio', value='DualAudio')
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')
rebulk.string('Fansub', value='Fansub', tags='has-neighbor')
rebulk.string('Fastsub', value='Fastsub', tags='has-neighbor')
season_words = build_or_pattern(["seasons?", "series?"])
complete_articles = build_or_pattern(["The"])
@ -66,38 +61,29 @@ def other(config): # pylint:disable=unused-argument
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.string('R5', 'RC', value='R5')
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'):
for value in (
'Screener', 'Remux', '3D', 'mHD', 'HDLight', 'HQ', 'DDC', 'HR', 'PAL', 'SECAM', 'NTSC',
'CC', 'LD', 'MD', '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('LDTV', value='LD')
rebulk.string('HD', value='HD', validator=None,
tags=['streaming_service.prefix', 'streaming_service.suffix'])
rebulk.regex('Full-?HD', 'FHD', value='Full HD', validator=None,
rebulk.regex('Full-?HD', 'FHD', value='FullHD', validator=None,
tags=['streaming_service.prefix', 'streaming_service.suffix'])
rebulk.regex('Ultra-?(?:HD)?', 'UHD', value='Ultra HD', validator=None,
rebulk.regex('Ultra-?(?:HD)?', 'UHD', value='UltraHD', validator=None,
tags=['streaming_service.prefix', 'streaming_service.suffix'])
rebulk.regex('Upscaled?', value='Upscaled')
for value in ('Complete', 'Classic', 'Bonus', 'Trailer', 'Retail',
for value in ('Complete', 'Classic', 'LiNE', 'Bonus', 'Trailer', 'FINAL', '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('DOCU', 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')
@ -106,28 +92,16 @@ def other(config): # pylint:disable=unused-argument
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.string('VO', 'OV', value='OV', tags='has-neighbor')
rebulk.regex('Scr(?:eener)?', value='Screener', validator=None,
tags=['other.validate.screener', 'source-prefix', 'source-suffix'])
tags=['other.validate.screener', 'format-prefix', 'format-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')
tags=['other.validate.mux', 'video-codec-prefix', 'format-suffix'])
rebulk.string('HC', value='Hardcoded Subtitles')
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)
rebulk.rules(ValidateHasNeighbor, ValidateHasNeighborAfter, ValidateHasNeighborBefore, ValidateScreenerRule,
ValidateMuxRule, ValidateHardcodedSubs, ValidateStreamingServiceNeighbor, ProperCountRule)
return rebulk
@ -142,7 +116,7 @@ class ProperCountRule(Rule):
properties = {'proper_count': [None]}
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
def when(self, matches, context):
propers = matches.named('other', lambda match: match.value == 'Proper')
if propers:
raws = {} # Count distinct raw values
@ -154,23 +128,11 @@ class ProperCountRule(Rule):
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 = []
@ -196,7 +158,6 @@ class ValidateHasNeighborBefore(Rule):
Validate tag has-neighbor-before that previous match exists.
"""
consequence = RemoveMatch
priority = 64
def when(self, matches, context):
ret = []
@ -216,7 +177,6 @@ class ValidateHasNeighborAfter(Rule):
Validate tag has-neighbor-after that next match exists.
"""
consequence = RemoveMatch
priority = 64
def when(self, matches, context):
ret = []
@ -241,8 +201,8 @@ class ValidateScreenerRule(Rule):
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):
format_match = matches.previous(screener, lambda match: match.name == 'format', 0)
if not format_match or matches.input_string[format_match.end:screener.start].strip(seps):
ret.append(screener)
return ret
@ -257,8 +217,8 @@ class ValidateMuxRule(Rule):
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:
format_match = matches.previous(mux, lambda match: match.name == 'format', 0)
if not format_match:
ret.append(mux)
return ret
@ -297,18 +257,16 @@ class ValidateStreamingServiceNeighbor(Rule):
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
predicate=lambda m: ('streaming_service.prefix' in m.tags or
'streaming_service.suffix' in m.tags)):
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):
@ -318,27 +276,6 @@ class ValidateStreamingServiceNeighbor(Rule):
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

@ -7,25 +7,20 @@ 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
def part():
"""
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})
rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, abbreviations=[dash], validator={'__parent__': seps_surround})
prefixes = config['prefixes']
prefixes = ['pt', 'part']
def validate_roman(match):
"""

@ -6,53 +6,22 @@ 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):
def release_group():
"""
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'))
rebulk = Rebulk()
expected_group = build_expected_function('expected_group')
@ -61,135 +30,42 @@ def release_group(config):
conflict_solver=lambda match, other: other,
disabled=lambda context: not context.get('expected_group'))
return rebulk.rules(
DashSeparatedReleaseGroup(clean_groupname),
SceneReleaseGroup(clean_groupname),
AnimeReleaseGroup
)
return rebulk.rules(SceneReleaseGroup, 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')
forbidden_groupnames = ['rip', 'by', 'for', 'par', 'pour', 'bonus']
_scene_previous_tags = ('release-group-prefix', )
groupname_ignore_seps = '[]{}()'
groupname_seps = ''.join([c for c in seps if c not in groupname_ignore_seps])
class DashSeparatedReleaseGroup(Rule):
def clean_groupname(string):
"""
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.
Removes and strip separators from input_string
:param string:
:type string:
:return:
:rtype:
"""
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)
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
_scene_previous_names = ['video_codec', 'format', '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']
to_append.append(releasegroup)
return to_remove, to_append
_scene_previous_tags = ['release-group-prefix']
class SceneReleaseGroup(Rule):
@ -203,12 +79,7 @@ class SceneReleaseGroup(Rule):
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
def when(self, matches, context):
# If a release_group is found before, ignore this kind of release_group rule.
ret = []
@ -216,8 +87,6 @@ class SceneReleaseGroup(Rule):
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)
@ -232,7 +101,7 @@ class SceneReleaseGroup(Rule):
"""
return match in titles[1:]
last_hole = matches.holes(start, end + 1, formatter=self.value_formatter,
last_hole = matches.holes(start, end + 1, formatter=clean_groupname,
ignore=keep_only_first_title,
predicate=lambda hole: cleanup(hole.value), index=-1)
@ -265,7 +134,7 @@ class SceneReleaseGroup(Rule):
# 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
group.formatter = clean_groupname
if group.value == last_hole.value:
last_hole.start = group.start + 1
last_hole.end = group.end - 1
@ -296,11 +165,11 @@ class AnimeReleaseGroup(Rule):
# If a release_group is found before, ignore this kind of release_group rule.
if matches.named('release_group'):
return to_remove, to_append
return
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
return
for filepart in marker_sorted(matches.markers.named('path'), matches):

@ -3,115 +3,67 @@
"""
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 rebulk import Rebulk, Rule, RemoveMatch
from ..common.validators import seps_surround
from ..common import dash, seps
from ...reutils import build_or_pattern
def screen_size(config):
def screen_size():
"""
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})',
def conflict_solver(match, other):
"""
Conflict solver for most screen_size.
"""
if other.name == 'screen_size':
if 'resolution' in other.tags:
# The chtouile to solve conflict in "720 x 432" string matching both 720p pattern
int_value = _digits_re.findall(match.raw)[-1]
if other.value.startswith(int_value):
return match
return other
return '__default__'
rebulk = Rebulk().string_defaults(ignore_case=True).regex_defaults(flags=re.IGNORECASE)
rebulk.defaults(name="screen_size", validator=seps_surround, conflict_solver=conflict_solver)
rebulk.regex(r"(?:\d{3,}(?:x|\*))?360(?:i|p?x?)", value="360p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?368(?:i|p?x?)", value="368p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?480(?:i|p?x?)", value="480p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?576(?:i|p?x?)", value="576p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?720(?:i|p?(?:50|60)?x?)", value="720p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?720(?:p(?:50|60)?x?)", value="720p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?720p?hd", value="720p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?900(?:i|p?x?)", value="900p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?1080i", value="1080i")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?1080p?x?", value="1080p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?1080(?:p(?:50|60)?x?)", value="1080p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?1080p?hd", value="1080p")
rebulk.regex(r"(?:\d{3,}(?:x|\*))?2160(?:i|p?x?)", value="4K")
rebulk.string('4k', value='4K')
_digits_re = re.compile(r'\d+')
rebulk.defaults(name="screen_size", validator=seps_surround)
rebulk.regex(r'\d{3,}-?(?:x|\*)-?\d{3,}',
formatter=lambda value: 'x'.join(_digits_re.findall(value)),
abbreviations=[dash],
tags=['resolution'],
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)
rebulk.rules(ScreenSizeOnlyOne, RemoveScreenSizeConflicts)
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.
Keep a single screen_size pet filepath part.
"""
consequence = RemoveMatch
@ -120,15 +72,15 @@ class ScreenSizeOnlyOne(Rule):
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:
if len(screensize) > 1:
to_remove.extend(screensize[1:])
return to_remove
class ResolveScreenSizeConflicts(Rule):
class RemoveScreenSizeConflicts(Rule):
"""
Resolve screen_size conflicts with season and episode matches.
Remove season and episode matches which conflicts with screen_size match.
"""
consequence = RemoveMatch
@ -143,21 +95,14 @@ class ResolveScreenSizeConflicts(Rule):
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)):
date = matches.previous(screensize, lambda match: match.name == 'date', 0)
if date and not matches.holes(date.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

@ -7,24 +7,23 @@ 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
from ..common import dash
def size(config): # pylint:disable=unused-argument
def size():
"""
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])
def format_size(value):
"""Format size using uppercase and no space."""
return re.sub(r'(?<=\d)[.](?=[^\d])', '', value.upper())
rebulk = 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'])
rebulk.regex(r'\d+\.?[mgt]b', r'\d+\.\d+[mgt]b', formatter=format_size, tags=['release-group-prefix'])
return rebulk

@ -1,201 +0,0 @@
#!/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

@ -8,150 +8,64 @@ 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
def streaming_service():
"""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 = Rebulk().string_defaults(ignore_case=True).regex_defaults(flags=re.IGNORECASE, abbreviations=[dash])
rebulk.defaults(name='streaming_service', tags=['format-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('AMZN', 'AmazonPrime', value='Amazon Prime')
rebulk.regex('Amazon-Prime', value='Amazon Prime')
rebulk.string('AS', 'AdultSwim', value='Adult Swim')
rebulk.regex('Adult-Swim', value='Adult Swim')
rebulk.string('iP', 'BBCiPlayer', value='BBC iPlayer')
rebulk.regex('BBC-iPlayer', value='BBC iPlayer')
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('CC', 'ComedyCentral', value='Comedy Central')
rebulk.regex('Comedy-Central', value='Comedy Central')
rebulk.string('CR', 'CrunchyRoll', value='Crunchy Roll')
rebulk.regex('Crunchy-Roll', value='Crunchy Roll')
rebulk.string('CW', 'TheCW', value='The CW')
rebulk.regex('The-CW', value='The CW')
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('DSNY', 'Disney', value='Disney')
rebulk.string('EPIX', 'ePix', value='ePix')
rebulk.string('HBO', 'HBOGo', value='HBO Go')
rebulk.regex('HBO-Go', value='HBO Go')
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('IFC', 'IFC', value='IFC')
rebulk.string('PBS', 'PBS', value='PBS')
rebulk.string('NATG', 'NationalGeographic', value='National Geographic')
rebulk.regex('National-Geographic', value='National Geographic')
rebulk.string('NBA', 'NBATV', 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('NF', 'Netflix', value='Netflix')
rebulk.string('iTunes', value='iTunes')
rebulk.string('RTE', value='RTÉ One')
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('SPKE', 'SpikeTV', 'Spike TV', value='Spike TV')
rebulk.string('SYFY', 'Syfy', value='Syfy')
rebulk.string('TFOU', 'TFou', value='TFou')
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('TVL', 'TVLand', '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)
@ -165,7 +79,7 @@ class ValidateStreamingService(Rule):
consequence = RemoveMatch
def when(self, matches, context):
"""Streaming service is always before source.
"""Streaming service is always before format.
:param matches:
:type matches: rebulk.match.Matches
@ -179,20 +93,16 @@ class ValidateStreamingService(Rule):
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 not has_other and \
(not next_match or matches.holes(service.end, next_match.start,
predicate=lambda match: match.value.strip(seps))) and \
(not previous_match or matches.holes(previous_match.end, service.start,
predicate=lambda match: match.value.strip(seps))):
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'))
# Current match is a valid streaming service, removing invalid closed caption (CC) matches
to_remove.extend(matches.named('other', predicate=lambda match: match.value == 'CC'))
return to_remove

@ -13,21 +13,16 @@ 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
def title():
"""
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)
rebulk = Rebulk().rules(TitleFromPosition, PreferTitleWithYear)
expected_title = build_expected_function('expected_title')
@ -99,7 +94,7 @@ class TitleBaseRule(Rule):
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')
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):
"""
@ -119,7 +114,7 @@ class TitleBaseRule(Rule):
:return:
:rtype:
"""
if match.name in ('language', 'country'):
if match.name in ['language', 'country']:
# Keep language if exactly matching the hole.
if len(hole.value) == len(match.raw):
return True
@ -132,7 +127,7 @@ class TitleBaseRule(Rule):
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):
if not other_languages:
return True
return False
@ -150,7 +145,7 @@ class TitleBaseRule(Rule):
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
def check_titles_in_filepart(self, filepart, matches, context):
"""
Find title in filepart (ignoring language)
"""
@ -159,11 +154,12 @@ class TitleBaseRule(Rule):
holes = matches.holes(start, end + 1, formatter=formatters(cleanup, reorder_title),
ignore=self.is_ignored,
predicate=lambda m: m.value)
predicate=lambda hole: hole.value)
holes = self.holes_process(holes, matches)
for hole in holes:
# pylint:disable=cell-var-from-loop
if not hole or (self.hole_filter and not self.hole_filter(hole, matches)):
continue
@ -174,8 +170,8 @@ class TitleBaseRule(Rule):
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)
# pylint:disable=undefined-loop-variable
trailing = matches.chain_before(hole.end, seps, predicate=lambda match: match == ignored_match)
if trailing:
should_keep = self.should_keep(ignored_match, to_keep, matches, filepart, hole, False)
if should_keep:
@ -192,7 +188,7 @@ class TitleBaseRule(Rule):
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)
predicate=lambda match: match == ignored_match)
if starting:
should_keep = self.should_keep(ignored_match, to_keep, matches, filepart, hole, True)
if should_keep:
@ -218,7 +214,7 @@ class TitleBaseRule(Rule):
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)
titles = hole.split(title_seps, lambda match: match.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]
@ -235,15 +231,14 @@ class TitleBaseRule(Rule):
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
return
fileparts = [filepart for filepart in list(marker_sorted(matches.markers.named('path'), matches))
if not self.filepart_filter or self.filepart_filter(filepart, matches)]
to_remove = []
# Priorize fileparts containing the year
years_fileparts = []
for filepart in fileparts:
@ -251,6 +246,7 @@ class TitleBaseRule(Rule):
if year_match:
years_fileparts.append(filepart)
ret = []
for filepart in fileparts:
try:
years_fileparts.remove(filepart)
@ -286,9 +282,6 @@ class TitleFromPosition(TitleBaseRule):
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):
"""
@ -309,7 +302,7 @@ class PreferTitleWithYear(Rule):
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')
group = matches.markers.at_match(year_match, lambda group: group.name == 'group')
if group:
with_year_in_group.append(title_match)
else:

@ -6,7 +6,6 @@ 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
@ -20,19 +19,13 @@ def _type(matches, value):
matches.append(Match(len(matches.input_string), len(matches.input_string), name='type', value=value))
def type_(config): # pylint:disable=unused-argument
def type_():
"""
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
return Rebulk().rules(TypeProcessor)
class TypeProcessor(CustomRule):
@ -52,10 +45,9 @@ class TypeProcessor(CustomRule):
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:
if episode or season or episode_details:
return 'episode'
film = matches.named('film')

@ -8,62 +8,42 @@ 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
def video_codec():
"""
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'},
rebulk = Rebulk().regex_defaults(flags=re.IGNORECASE, abbreviations=[dash]).string_defaults(ignore_case=True)
rebulk.defaults(name="video_codec", tags=['format-suffix', 'streaming_service.suffix'])
rebulk.regex(r"Rv\d{2}", value="Real")
rebulk.regex("Mpeg2", value="Mpeg2")
rebulk.regex("DVDivX", "DivX", value="DivX")
rebulk.regex("XviD", value="XviD")
rebulk.regex("[hx]-?264(?:-?AVC(HD)?)?", "MPEG-?4(?:-?AVC(HD)?)", "AVC(?:HD)?", value="h264")
rebulk.regex("[hx]-?265(?:-?HEVC)?", "HEVC", value="h265")
rebulk.regex('(?P<video_codec>hevc)(?P<video_profile>10)', value={'video_codec': 'h265', 'video_profile': '10bit'},
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.defaults(name="video_profile", validator=seps_surround)
rebulk.regex('10.?bits?', 'Hi10P?', 'YUV420P10', value='10bit')
rebulk.regex('8.?bits?', value='8bit')
rebulk.string('BP', value='BP', tags='video_profile.rule')
rebulk.string('XP', 'EP', value='XP', tags='video_profile.rule')
rebulk.string('MP', value='MP', tags='video_profile.rule')
rebulk.string('HP', 'HiP', value='HP', tags='video_profile.rule')
rebulk.regex('Hi422P', value='Hi422P', tags='video_profile.rule')
rebulk.regex('Hi444PP', value='Hi444PP', tags='video_profile.rule')
rebulk.string('DXVA', value='DXVA', name='video_api')
rebulk.rules(ValidateVideoCodec, VideoProfileRule)
@ -72,14 +52,11 @@ def video_codec(config): # pylint:disable=unused-argument
class ValidateVideoCodec(Rule):
"""
Validate video_codec with source property or separated
Validate video_codec with format 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'):
@ -100,9 +77,6 @@ class VideoProfileRule(Rule):
"""
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 = []

@ -9,32 +9,28 @@ 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):
def website():
"""
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 = 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']
safe_tlds = ['com', 'org', 'net'] # For sure a website extension
safe_subdomains = ['www'] # For sure a website subdomain
safe_prefix = ['co', 'com', 'org', 'net'] # Those words before a tlds are sure
website_prefixes = ['from']
rebulk.regex(r'(?:[^a-z0-9]|^)((?:'+build_or_pattern(safe_subdomains) +
r'\.)+(?:[a-z-]+\.)+(?:'+build_or_pattern(tlds) +

@ -1,335 +0,0 @@
? 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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -12,18 +12,18 @@
? +DD
? +Dolby Digital
? +AC3
: audio_codec: Dolby Digital
: audio_codec: AC3
? +DDP
? +DD+
? +EAC3
: audio_codec: Dolby Digital Plus
: audio_codec: EAC3
? +DolbyAtmos
? +Dolby Atmos
? +Atmos
? -Atmosphere
: audio_codec: Dolby Atmos
: audio_codec: DolbyAtmos
? +AAC
: audio_codec: AAC
@ -36,34 +36,33 @@
? +True-HD
? +trueHD
: audio_codec: Dolby TrueHD
: audio_codec: TrueHD
? +True-HD51
? +trueHD51
: audio_codec: Dolby TrueHD
: audio_codec: TrueHD
audio_channels: '5.1'
? +DTSHD
? +DTS HD
? +DTS-HD
: audio_codec: DTS-HD
: audio_codec: DTS
audio_profile: HD
? +DTS-HDma
? +DTSMA
: audio_codec: DTS-HD
audio_profile: Master Audio
: audio_codec: DTS
audio_profile: HDMA
? +AC3-hq
: audio_codec: Dolby Digital
audio_profile: High Quality
: audio_codec: AC3
audio_profile: HQ
? +AAC-HE
: audio_codec: AAC
audio_profile: High Efficiency
audio_profile: HE
? +AAC-LC
: audio_codec: AAC
audio_profile: Low Complexity
audio_profile: LC
? +AAC2.0
? +AAC20
@ -91,41 +90,8 @@
? DD5.1
? DD51
: audio_codec: Dolby Digital
: audio_codec: AC3
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

@ -7,4 +7,4 @@
? Some.Title-DVDRIP-x264-CDP
: cd: !!null
release_group: CDP
video_codec: H.264
video_codec: h264

@ -8,6 +8,3 @@
? This.is.us.title
: title: This is us title
? This.Is.Us
: options: --no-embedded-config
title: This Is Us

@ -7,57 +7,25 @@
? Collector
? Collector Edition
? Edition Collector
: edition: Collector
: edition: Collector Edition
? Special Edition
? Edition Special
? -Special
: edition: Special
: edition: Special Edition
? Criterion Edition
? Edition Criterion
? CC
? -Criterion
: edition: Criterion
: edition: Criterion Edition
? Deluxe
? Deluxe Edition
? Edition Deluxe
: edition: Deluxe
: edition: Deluxe Edition
? 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]

@ -156,7 +156,7 @@
? Show.Name.Season.1.3&5.HDTV.XviD-GoodGroup[SomeTrash]
? Show.Name.Season.1.3 and 5.HDTV.XviD-GoodGroup[SomeTrash]
: source: HDTV
: format: HDTV
release_group: GoodGroup[SomeTrash]
season:
- 1
@ -164,12 +164,12 @@
- 5
title: Show Name
type: episode
video_codec: Xvid
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
: format: HDTV
release_group: GoodGroup[SomeTrash]
season:
- 1
@ -179,19 +179,18 @@
- 5
title: Show Name
type: episode
video_codec: Xvid
video_codec: XviD
? The.Get.Down.S01EP01.FRENCH.720p.WEBRIP.XVID-STR
: episode: 1
source: Web
other: Rip
format: WEBRip
language: fr
release_group: STR
screen_size: 720p
season: 1
title: The Get Down
type: episode
video_codec: Xvid
video_codec: XviD
? My.Name.Is.Earl.S01E01-S01E21.SWE-SUB
: episode:
@ -270,10 +269,4 @@
? Episode71
? Episode 71
: episode: 71
? S01D02.3-5-GROUP
: disc: [2, 3, 4, 5]
? S01D02&4-6&8
: disc: [2, 4, 5, 6, 8]
: episode: 71

@ -0,0 +1,138 @@
# Multiple input strings having same expected results can be chained.
# Use - marker to check inputs that should not match results.
? +VHS
? +VHSRip
? +VHS-Rip
? +VhS_rip
? +VHS.RIP
? -VHSAnythingElse
? -SomeVHS stuff
? -VH
? -VHx
? -VHxRip
: format: VHS
? +Cam
? +CamRip
? +CaM Rip
? +Cam_Rip
? +cam.rip
: format: Cam
? +Telesync
? +TS
? +HD TS
? -Hd.Ts # ts file extension
? -HD.TS # ts file extension
? +Hd-Ts
: format: Telesync
? +Workprint
? +workPrint
? +WorkPrint
? +WP
? -Work Print
: format: Workprint
? +Telecine
? +teleCine
? +TC
? -Tele Cine
: format: Telecine
? +PPV
? +ppv-rip
: format: PPV
? -TV
? +SDTV
? +SDTVRIP
? +Rip sd tv
? +TvRip
? +Rip TV
: format: TV
? +DVB
? +DVB-Rip
? +DvBRiP
? +pdTV
? +Pd Tv
: format: DVB
? +DVD
? +DVD-RIP
? +video ts
? +DVDR
? +DVD 9
? +dvd 5
? -dvd ts
: format: DVD
-format: ts
? +HDTV
? +tv rip hd
? +HDtv Rip
? +HdRip
: format: HDTV
? +VOD
? +VodRip
? +vod rip
: format: VOD
? +webrip
? +Web Rip
? +webdlrip
? +web dl rip
? +webcap
? +web cap
: format: WEBRip
? +webdl
? +Web DL
? +webHD
? +WEB hd
? +web
: format: WEB-DL
? +HDDVD
? +hd dvd
? +hdDvdRip
: format: HD-DVD
? +BluRay
? +BluRay rip
? +BD
? +BR
? +BDRip
? +BR rip
? +BD5
? +BD9
? +BD25
? +bd50
: format: BluRay
? XVID.NTSC.DVDR.nfo
: format: DVD
? AHDTV
: format: AHDTV
? dsr
? dsrip
? ds rip
? dsrrip
? dsr rip
? satrip
? sat rip
? dth
? dthrip
? dth rip
: format: SATRip
? HDTC
: format: HDTC
? UHDTV
? UHDRip
: format: UHDTV

@ -36,12 +36,4 @@
? +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
subtitle_language: Swedish

@ -12,22 +12,22 @@
? +AudioFixed
? +Audio Fix
? +Audio Fixed
: other: Audio Fixed
: other: AudioFix
? +SyncFix
? +SyncFixed
? +Sync Fix
? +Sync Fixed
: other: Sync Fixed
: other: SyncFix
? +DualAudio
? +Dual Audio
: other: Dual Audio
: other: DualAudio
? +ws
? +WideScreen
? +Wide Screen
: other: Widescreen
: other: WideScreen
# Fix and Real must be surround by others properties to be matched.
? DVD.Real.XViD
@ -58,20 +58,18 @@
proper_count: 1
? XViD.Fansub
: other: Fan Subtitled
: other: Fansub
? XViD.Fastsub
: other: Fast Subtitled
: other: Fastsub
? +Season Complete
? -Complete
: other: Complete
? R5
: other: Region 5
? RC
: other: Region C
: other: R5
? PreAir
? Pre Air
@ -92,23 +90,28 @@
? FHD
? FullHD
? Full HD
: other: Full HD
: other: FullHD
? UHD
? Ultra
? UltraHD
? Ultra HD
: other: Ultra HD
: other: UltraHD
? mHD # ??
: other: mHD
? HDLight
: other: Micro HD
: other: HDLight
? HQ
: other: High Quality
: other: HQ
? ddc
: other: DDC
? hr
: other: High Resolution
: other: HR
? PAL
: other: PAL
@ -119,14 +122,15 @@
? NTSC
: other: NTSC
? LDTV
: other: Low Definition
? CC
: other: CC
? LD
: other: Line Dubbed
? LDTV
: other: LD
? MD
: other: Mic Dubbed
: other: MD
? -The complete movie
: other: Complete
@ -135,38 +139,16 @@
: title: The complete movie
? +AC3-HQ
: audio_profile: High Quality
: audio_profile: HQ
? Other-HQ
: other: High Quality
: other: HQ
? reenc
? re-enc
? re-encoded
? reencoded
: other: 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
: other: Converted

@ -42,30 +42,30 @@
? Show.Name.x264-byEMP
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: byEMP
? Show.Name.x264-NovaRip
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: NovaRip
? Show.Name.x264-PARTiCLE
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: PARTiCLE
? Show.Name.x264-POURMOi
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: POURMOi
? Show.Name.x264-RipPourBox
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: RipPourBox
? Show.Name.x264-RiPRG
: title: Show Name
video_codec: H.264
video_codec: h264
release_group: RiPRG

@ -2,258 +2,68 @@
# Use - marker to check inputs that should not match results.
? +360p
? +360px
? -360
? +360i
? "+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
? +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
? "+480"
? +500x480
: screen_size: 480p
? +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
? "+576"
? +500x576
: screen_size: 576p
? +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
? +720i
? "+720"
? +500x720
: screen_size: 500x720
aspect_ratio: 0.694
: screen_size: 720p
? +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
? "+900"
? +500x900
: screen_size: 900p
? +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
? "+1080"
? +500x1080
: 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
? +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
: screen_size: 4K
? Test.File.720hd.bluray
? Test.File.720p24
? Test.File.720p30
? Test.File.720p50
? Test.File.720p60
? Test.File.720p120
: screen_size: 720p

@ -1,323 +0,0 @@
# 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

@ -6,19 +6,15 @@
? Rv30
? rv40
? -xrv40
: video_codec: RealVideo
: video_codec: Real
? mpeg2
? MPEG2
? MPEG-2
? mpg2
? H262
? H.262
? x262
? -mpeg
? -mpeg 2 # Not sure if we should ignore this one ...
? -xmpeg2
? -mpeg2x
: video_codec: MPEG-2
: video_codec: Mpeg2
? DivX
? -div X
@ -30,29 +26,19 @@
? XviD
? xvid
? -x vid
: video_codec: Xvid
? h263
? x263
? h.263
: video_codec: H.263
: video_codec: XviD
? 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
: video_codec: h264
? h265
? x265
@ -61,27 +47,13 @@
? hevc
? -h 264
? -x264
: video_codec: H.265
: video_codec: h265
? hevc10
? HEVC-YUV420P10
: video_codec: H.265
color_depth: 10-bit
: video_codec: h265
video_profile: 10bit
? 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
: video_codec: h265
video_profile: HP

File diff suppressed because it is too large Load Diff

@ -27,14 +27,6 @@ def test_forced_binary():
assert ret and 'title' in ret and isinstance(ret['title'], six.binary_type)
@pytest.mark.skipif('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

@ -53,14 +53,6 @@ if six.PY2:
"""
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()

@ -136,7 +136,7 @@ class TestYml(object):
Use $ marker to check inputs that should not match results.
"""
options_re = re.compile(r'^([ +-]+)(.*)')
options_re = re.compile(r'^([ \+-]+)(.*)')
files, ids = files_and_ids(filename_predicate)
@ -149,7 +149,7 @@ class TestYml(object):
@pytest.mark.parametrize('filename', files, ids=ids)
def test(self, filename, caplog):
caplog.set_level(logging.INFO)
caplog.setLevel(logging.INFO)
with open(os.path.join(__location__, filename), 'r', encoding='utf-8') as infile:
data = yaml.load(infile, OrderedDictYAMLLoader)
entries = Results()
@ -274,10 +274,10 @@ class TestYml(object):
if negates_key:
entry.valid.append((expected_key, expected_value))
else:
entry.different.append((expected_key, expected_value, result[result_key]))
entry.different.append((expected_key, expected_value, result[expected_key]))
else:
if negates_key:
entry.different.append((expected_key, expected_value, result[result_key]))
entry.different.append((expected_key, expected_value, result[expected_key]))
else:
entry.valid.append((expected_key, expected_value))
elif not negates_key:

@ -3,9 +3,9 @@
title: Fear and Loathing in Las Vegas
year: 1998
screen_size: 720p
source: HD-DVD
format: HD-DVD
audio_codec: DTS
video_codec: H.264
video_codec: h264
release_group: ESiR
? Series/Duckman/Duckman - 101 (01) - 20021107 - I, Duckman.avi
@ -36,9 +36,8 @@
episode_format: Minisode
episode: 1
episode_title: Good Cop Bad Cop
source: Web
other: Rip
video_codec: Xvid
format: WEBRip
video_codec: XviD
? Series/Kaamelott/Kaamelott - Livre V - Ep 23 - Le Forfait.avi
: type: episode
@ -51,10 +50,10 @@
title: The Doors
year: 1991
date: 2008-03-09
source: Blu-ray
format: BluRay
screen_size: 720p
audio_codec: Dolby Digital
video_codec: H.264
audio_codec: AC3
video_codec: h264
release_group: HiS@SiLUHD
language: english
website: sharethefiles.com
@ -64,15 +63,14 @@
title: MASH
year: 1970
video_codec: DivX
source: DVD
other: [Dual Audio, Rip]
format: DVD
? the.mentalist.501.hdtv-lol.mp4
: type: episode
title: the mentalist
season: 5
episode: 1
source: HDTV
format: HDTV
release_group: lol
? the.simpsons.2401.hdtv-lol.mp4
@ -80,7 +78,7 @@
title: the simpsons
season: 24
episode: 1
source: HDTV
format: HDTV
release_group: lol
? Homeland.S02E01.HDTV.x264-EVOLVE.mp4
@ -88,8 +86,8 @@
title: Homeland
season: 2
episode: 1
source: HDTV
video_codec: H.264
format: HDTV
video_codec: h264
release_group: EVOLVE
? /media/Band_of_Brothers-e01-Currahee.mkv
@ -117,7 +115,7 @@
title: new girl
season: 1
episode: 17
source: HDTV
format: HDTV
release_group: lol
? The.Office.(US).1x03.Health.Care.HDTV.XviD-LOL.avi
@ -127,8 +125,8 @@
season: 1
episode: 3
episode_title: Health Care
source: HDTV
video_codec: Xvid
format: HDTV
video_codec: XviD
release_group: LOL
? The_Insider-(1999)-x02-60_Minutes_Interview-1996.mp4
@ -156,18 +154,18 @@
season: 56
episode: 6
screen_size: 720p
source: HDTV
video_codec: H.264
format: HDTV
video_codec: h264
? 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
format: BluRay
audio_codec: DTS
audio_profile: HDMA
video_codec: h264
release_group: PublicHD
audio_channels: "5.1"
@ -176,10 +174,10 @@
title: White House Down
year: 2013
screen_size: 1080p
source: Blu-ray
audio_codec: DTS-HD
audio_profile: Master Audio
video_codec: H.264
format: BluRay
audio_codec: DTS
audio_profile: HDMA
video_codec: h264
release_group: PublicHD
audio_channels: "5.1"
@ -190,10 +188,10 @@
season: 1
episode: 1
screen_size: 720p
source: Web
format: WEB-DL
audio_channels: "5.1"
video_codec: H.264
audio_codec: Dolby Digital
video_codec: h264
audio_codec: AC3
release_group: NTb
? Despicable.Me.2.2013.1080p.BluRay.x264-VeDeTT.nfo
@ -201,39 +199,37 @@
title: Despicable Me 2
year: 2013
screen_size: 1080p
source: Blu-ray
video_codec: H.264
format: BluRay
video_codec: h264
release_group: VeDeTT
? Le Cinquieme Commando 1971 SUBFORCED FRENCH DVDRiP XViD AC3 Bandix.mkv
: type: movie
audio_codec: Dolby Digital
source: DVD
other: Rip
audio_codec: AC3
format: DVD
release_group: Bandix
subtitle_language: French
title: Le Cinquieme Commando
video_codec: Xvid
video_codec: XviD
year: 1971
? Le Seigneur des Anneaux - La Communauté de l'Anneau - Version Longue - BDRip.mkv
: type: movie
format: BluRay
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
video_codec: h264
year: 1983
other: PAL
? Retour de Flammes (Gregor Schnitzler 2003) FULL DVD.iso
: type: movie
source: DVD
format: DVD
title: Retour de Flammes
type: movie
year: 2003
@ -254,16 +250,16 @@
: type: movie
year: 2014
title: A Common Title
edition: Special
edition: Special Edition
? Downton.Abbey.2013.Christmas.Special.HDTV.x264-FoV.mp4
: type: episode
year: 2013
title: Downton Abbey
episode_title: Christmas Special
video_codec: H.264
video_codec: h264
release_group: FoV
source: HDTV
format: HDTV
episode_details: Special
? Doctor_Who_2013_Christmas_Special.The_Time_of_The_Doctor.HD
@ -284,10 +280,10 @@
? Robot Chicken S06-Born Again Virgin Christmas Special HDTV x264.avi
: type: episode
title: Robot Chicken
source: HDTV
format: HDTV
season: 6
episode_title: Born Again Virgin Christmas Special
video_codec: H.264
video_codec: h264
episode_details: Special
? Wicked.Tuna.S03E00.Head.To.Tail.Special.HDTV.x264-YesTV
@ -297,14 +293,14 @@
release_group: YesTV
season: 3
episode: 0
video_codec: H.264
source: HDTV
video_codec: h264
format: HDTV
episode_details: Special
? The.Voice.UK.S03E12.HDTV.x264-C4TV
: episode: 12
video_codec: H.264
source: HDTV
video_codec: h264
format: HDTV
title: The Voice
release_group: C4TV
season: 3
@ -321,21 +317,21 @@
? FlexGet.S01E02.TheName.HDTV.xvid
: episode: 2
source: HDTV
format: HDTV
season: 1
title: FlexGet
episode_title: TheName
type: episode
video_codec: Xvid
video_codec: XviD
? FlexGet.S01E02.TheName.HDTV.xvid
: episode: 2
source: HDTV
format: HDTV
season: 1
title: FlexGet
episode_title: TheName
type: episode
video_codec: Xvid
video_codec: XviD
? some.series.S03E14.Title.Here.720p
: episode: 14
@ -366,7 +362,7 @@
? Something.Season.2.1of4.Ep.Title.HDTV.torrent
: episode_count: 4
episode: 1
source: HDTV
format: HDTV
season: 2
title: Something
episode_title: Title
@ -376,7 +372,7 @@
? Show-A (US) - Episode Title S02E09 hdtv
: country: US
episode: 9
source: HDTV
format: HDTV
season: 2
title: Show-A
type: episode
@ -406,25 +402,23 @@
type: movie
? Movies/El Bosque Animado (1987)/El.Bosque.Animado.[Jose.Luis.Cuerda.1987].[Xvid-Dvdrip-720 * 432].avi
: source: DVD
other: Rip
: format: DVD
screen_size: 720x432
title: El Bosque Animado
video_codec: Xvid
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
: format: DVD
screen_size: 720x432
title: El Bosque Animado
video_codec: Xvid
video_codec: XviD
year: 1987
type: movie
? 2009.shoot.fruit.chan.multi.dvd9.pal
: source: DVD
: format: DVD
language: mul
other: PAL
title: shoot fruit chan
@ -432,7 +426,7 @@
year: 2009
? 2009.shoot.fruit.chan.multi.dvd5.pal
: source: DVD
: format: DVD
language: mul
other: PAL
title: shoot fruit chan
@ -441,25 +435,25 @@
? The.Flash.2014.S01E01.PREAIR.WEBRip.XviD-EVO.avi
: episode: 1
source: Web
other: [Preair, Rip]
format: WEBRip
other: Preair
release_group: EVO
season: 1
title: The Flash
type: episode
video_codec: Xvid
video_codec: XviD
year: 2014
? Ice.Lake.Rebels.S01E06.Ice.Lake.Games.720p.HDTV.x264-DHD
: episode: 6
source: HDTV
format: HDTV
release_group: DHD
screen_size: 720p
season: 1
title: Ice Lake Rebels
episode_title: Ice Lake Games
type: episode
video_codec: H.264
video_codec: h264
? The League - S06E10 - Epi Sexy.mkv
: episode: 10
@ -469,23 +463,23 @@
type: episode
? Stay (2005) [1080p]/Stay.2005.1080p.BluRay.x264.YIFY.mp4
: source: Blu-ray
: format: BluRay
release_group: YIFY
screen_size: 1080p
title: Stay
type: movie
video_codec: H.264
video_codec: h264
year: 2005
? /media/live/A/Anger.Management.S02E82.720p.HDTV.X264-DIMENSION.mkv
: source: HDTV
: format: HDTV
release_group: DIMENSION
screen_size: 720p
title: Anger Management
type: episode
season: 2
episode: 82
video_codec: H.264
video_codec: h264
? "[Figmentos] Monster 34 - At the End of Darkness [781219F1].mkv"
: type: episode
@ -498,7 +492,7 @@
? Game.of.Thrones.S05E07.720p.HDTV-KILLERS.mkv
: type: episode
episode: 7
source: HDTV
format: HDTV
release_group: KILLERS
screen_size: 720p
season: 5
@ -507,7 +501,7 @@
? Game.of.Thrones.S05E07.HDTV.720p-KILLERS.mkv
: type: episode
episode: 7
source: HDTV
format: HDTV
release_group: KILLERS
screen_size: 720p
season: 5
@ -525,8 +519,8 @@
title: Star Trek Into Darkness
year: 2013
screen_size: 720p
source: Web
video_codec: H.264
format: WEB-DL
video_codec: h264
release_group: publichd
? /var/medias/series/The Originals/Season 02/The.Originals.S02E15.720p.HDTV.X264-DIMENSION.mkv
@ -535,8 +529,8 @@
season: 2
episode: 15
screen_size: 720p
source: HDTV
video_codec: H.264
format: HDTV
video_codec: h264
release_group: DIMENSION
? Test.S01E01E07-FooBar-Group.avi
@ -545,211 +539,202 @@
- 1
- 7
episode_title: FooBar-Group # Make sure it doesn't conflict with uuid
mimetype: video/x-msvideo
season: 1
title: Test
type: episode
? TEST.S01E02.2160p.NF.WEBRip.x264.DD5.1-ABC
: audio_channels: '5.1'
audio_codec: Dolby Digital
audio_codec: AC3
episode: 2
source: Web
other: Rip
format: WEBRip
release_group: ABC
screen_size: 2160p
screen_size: 4K
season: 1
streaming_service: Netflix
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? TEST.2015.12.30.720p.WEBRip.h264-ABC
: date: 2015-12-30
source: Web
other: Rip
format: WEBRip
release_group: ABC
screen_size: 720p
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? 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
format: WEBRip
release_group: ABC
screen_size: 1080p
season: 1
streaming_service: Netflix
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? 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
format: WEBRip
release_group: ABC
screen_size: 1080p
season: 1
streaming_service: Netflix
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? 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
format: WEBRip
release_group: ABC
screen_size: 1080p
season: 1
streaming_service: Netflix
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? TEST.S05E02.720p.iP.WEBRip.AAC2.0.H264-ABC
: audio_channels: '2.0'
audio_codec: AAC
episode: 2
source: Web
other: Rip
format: WEBRip
release_group: ABC
screen_size: 720p
season: 5
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? TEST.S03E07.720p.WEBRip.AAC2.0.x264-ABC
: audio_channels: '2.0'
audio_codec: AAC
episode: 7
source: Web
other: Rip
format: WEBRip
release_group: ABC
screen_size: 720p
season: 3
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? 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
format: WEBRip
release_group: ABC
screen_size: 1080p
season: 15
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? 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
format: WEBRip
release_group: ABC
screen_size: 720p
season: 11
title: TEST
type: episode
video_codec: H.264
video_codec: h264
? TEST.2015.1080p.HC.WEBRip.x264.AAC2.0-ABC
: audio_channels: '2.0'
audio_codec: AAC
source: Web
other: Rip
format: WEBRip
release_group: ABC
screen_size: 1080p
title: TEST
type: movie
video_codec: H.264
video_codec: h264
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
audio_codec: DTS
audio_profile: HDMA
format: BluRay
other: 3D
release_group: ABC
screen_size: 1080p
title: TEST
type: movie
video_codec: H.264
video_codec: h264
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
audio_codec: DTS
audio_profile: HDMA
format: BluRay
other: 3D
release_group: ABC
screen_size: 1080p
title: TEST
type: movie
video_codec: H.264
video_codec: h264
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
- DTS
- TrueHD
- DolbyAtmos
audio_profile: HDMA
format: BluRay
other: 3D
release_group: ABC
screen_size: 1080p
title: TEST
type: movie
video_codec: H.264
video_codec: h264
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
- DTS
- TrueHD
- DolbyAtmos
audio_profile: HDMA
format: BluRay
other: 3D
release_group: ABC
screen_size: 1080p
title: TEST
type: movie
video_codec: H.264
video_codec: h264
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
- DTS
- TrueHD
- DolbyAtmos
audio_profile: HDMA
format: BluRay
other: Remux
release_group: ABC
screen_size: 1080p
@ -758,24 +743,23 @@
year: 2015
? Gangs of New York 2002 REMASTERED 1080p BluRay x264-AVCHD
: source: Blu-ray
: format: BluRay
edition: Remastered
screen_size: 1080p
title: Gangs of New York
type: movie
video_codec: H.264
video_codec: h264
year: 2002
? Peep.Show.S06E02.DVDrip.x264-faks86.mkv
: container: mkv
episode: 2
source: DVD
other: Rip
format: DVD
release_group: faks86
season: 6
title: Peep Show
type: episode
video_codec: H.264
video_codec: h264
# Episode title is indeed 'October 8, 2014'
# https://thetvdb.com/?tab=episode&seriesid=82483&seasonid=569935&id=4997362&lid=7
@ -790,155 +774,28 @@
? Red.Rock.S02E59.WEB-DLx264-JIVE
: episode: 59
season: 2
source: Web
format: WEB-DL
release_group: JIVE
title: Red Rock
type: episode
video_codec: H.264
video_codec: h264
? Pawn.Stars.S12E31.Deals.On.Wheels.PDTVx264-JIVE
: episode: 31
episode_title: Deals On Wheels
season: 12
source: Digital TV
format: DVB
release_group: JIVE
title: Pawn Stars
type: episode
video_codec: H.264
video_codec: h264
? Duck.Dynasty.S09E09.Van.He-llsing.HDTVx264-JIVE
: episode: 9
episode_title: Van He-llsing
season: 9
source: HDTV
format: 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
video_codec: h264

@ -3,7 +3,6 @@
"""
Options
"""
try:
from collections import OrderedDict
except ImportError: # pragma: no-cover
@ -12,8 +11,6 @@ import babelfish
import yaml
from .rules.common.quantity import BitRate, FrameRate, Size
class OrderedDictYAMLLoader(yaml.Loader):
"""
@ -64,18 +61,11 @@ class CustomDumper(yaml.SafeDumper):
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())
return dumper.represent_dict(data)
CustomDumper.add_representer(OrderedDict, ordered_dict_representer)

@ -238,7 +238,10 @@ def guess_matches(video, guess, partial=False):
if video.resolution and 'screen_size' in guess and guess['screen_size'] == video.resolution:
matches.add('resolution')
# format
if video.format and 'format' in guess and guess['format'].lower() == video.format.lower():
# Guessit may return a list for `format`, which indicates a conflict in the guessing.
# We should match `format` only when it returns single value to avoid false `format` matches
if video.format and guess.get('format') and not isinstance(guess['format'], list) \
and guess['format'].lower() == video.format.lower():
matches.add('format')
# video_codec
if video.video_codec and 'video_codec' in guess and guess['video_codec'] == video.video_codec:

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Semantic Org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

@ -1,7 +0,0 @@
# CSS Distribution
This repository is automatically synced with the main [Semantic UI](https://github.com/Semantic-Org/Semantic-UI) repository to provide lightweight CSS only version of Semantic UI.
This package **does not support theming** and includes generated CSS files of the default theme only.
You can view more on Semantic UI at [LearnSemantic.com](http://www.learnsemantic.com) and [Semantic-UI.com](http://www.semantic-ui.com)

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Accordion
* # Semantic UI 2.4.0 - Accordion
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Accordion
* # Semantic UI 2.4.0 - Accordion
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Accordion
* # Semantic UI 2.4.0 - Accordion
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Ad
* # Semantic UI 2.4.0 - Ad
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Ad
* # Semantic UI 2.4.0 - Ad
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - API
* # Semantic UI 2.4.0 - API
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Breadcrumb
* # Semantic UI 2.4.0 - Breadcrumb
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Breadcrumb
* # Semantic UI 2.4.0 - Breadcrumb
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Button
* # Semantic UI 2.4.0 - Button
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Button
* # Semantic UI 2.4.0 - Button
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Item
* # Semantic UI 2.4.0 - Item
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Item
* # Semantic UI 2.4.0 - Item
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Checkbox
* # Semantic UI 2.4.0 - Checkbox
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Checkbox
* # Semantic UI 2.4.0 - Checkbox
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Checkbox
* # Semantic UI 2.4.0 - Checkbox
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,274 +0,0 @@
/*!
* # Semantic UI 2.0.0 - Colorize
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2015 Contributors
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
;(function ( $, window, document, undefined ) {
"use strict";
$.fn.colorize = function(parameters) {
var
settings = ( $.isPlainObject(parameters) )
? $.extend(true, {}, $.fn.colorize.settings, parameters)
: $.extend({}, $.fn.colorize.settings),
// hoist arguments
moduleArguments = arguments || false
;
$(this)
.each(function(instanceIndex) {
var
$module = $(this),
mainCanvas = $('<canvas />')[0],
imageCanvas = $('<canvas />')[0],
overlayCanvas = $('<canvas />')[0],
backgroundImage = new Image(),
// defs
mainContext,
imageContext,
overlayContext,
image,
imageName,
width,
height,
// shortcuts
colors = settings.colors,
paths = settings.paths,
namespace = settings.namespace,
error = settings.error,
// boilerplate
instance = $module.data('module-' + namespace),
module
;
module = {
checkPreconditions: function() {
module.debug('Checking pre-conditions');
if( !$.isPlainObject(colors) || $.isEmptyObject(colors) ) {
module.error(error.undefinedColors);
return false;
}
return true;
},
async: function(callback) {
if(settings.async) {
setTimeout(callback, 0);
}
else {
callback();
}
},
getMetadata: function() {
module.debug('Grabbing metadata');
image = $module.data('image') || settings.image || undefined;
imageName = $module.data('name') || settings.name || instanceIndex;
width = settings.width || $module.width();
height = settings.height || $module.height();
if(width === 0 || height === 0) {
module.error(error.undefinedSize);
}
},
initialize: function() {
module.debug('Initializing with colors', colors);
if( module.checkPreconditions() ) {
module.async(function() {
module.getMetadata();
module.canvas.create();
module.draw.image(function() {
module.draw.colors();
module.canvas.merge();
});
$module
.data('module-' + namespace, module)
;
});
}
},
redraw: function() {
module.debug('Redrawing image');
module.async(function() {
module.canvas.clear();
module.draw.colors();
module.canvas.merge();
});
},
change: {
color: function(colorName, color) {
module.debug('Changing color', colorName);
if(colors[colorName] === undefined) {
module.error(error.missingColor);
return false;
}
colors[colorName] = color;
module.redraw();
}
},
canvas: {
create: function() {
module.debug('Creating canvases');
mainCanvas.width = width;
mainCanvas.height = height;
imageCanvas.width = width;
imageCanvas.height = height;
overlayCanvas.width = width;
overlayCanvas.height = height;
mainContext = mainCanvas.getContext('2d');
imageContext = imageCanvas.getContext('2d');
overlayContext = overlayCanvas.getContext('2d');
$module
.append( mainCanvas )
;
mainContext = $module.children('canvas')[0].getContext('2d');
},
clear: function(context) {
module.debug('Clearing canvas');
overlayContext.fillStyle = '#FFFFFF';
overlayContext.fillRect(0, 0, width, height);
},
merge: function() {
if( !$.isFunction(mainContext.blendOnto) ) {
module.error(error.missingPlugin);
return;
}
mainContext.putImageData( imageContext.getImageData(0, 0, width, height), 0, 0);
overlayContext.blendOnto(mainContext, 'multiply');
}
},
draw: {
image: function(callback) {
module.debug('Drawing image');
callback = callback || function(){};
if(image) {
backgroundImage.src = image;
backgroundImage.onload = function() {
imageContext.drawImage(backgroundImage, 0, 0);
callback();
};
}
else {
module.error(error.noImage);
callback();
}
},
colors: function() {
module.debug('Drawing color overlays', colors);
$.each(colors, function(colorName, color) {
settings.onDraw(overlayContext, imageName, colorName, color);
});
}
},
debug: function(message, variableName) {
if(settings.debug) {
if(variableName !== undefined) {
console.info(settings.name + ': ' + message, variableName);
}
else {
console.info(settings.name + ': ' + message);
}
}
},
error: function(errorMessage) {
console.warn(settings.name + ': ' + errorMessage);
},
invoke: function(methodName, context, methodArguments) {
var
method
;
methodArguments = methodArguments || Array.prototype.slice.call( arguments, 2 );
if(typeof methodName == 'string' && instance !== undefined) {
methodName = methodName.split('.');
$.each(methodName, function(index, name) {
if( $.isPlainObject( instance[name] ) ) {
instance = instance[name];
return true;
}
else if( $.isFunction( instance[name] ) ) {
method = instance[name];
return true;
}
module.error(settings.error.method);
return false;
});
}
return ( $.isFunction( method ) )
? method.apply(context, methodArguments)
: false
;
}
};
if(instance !== undefined && moduleArguments) {
// simpler than invoke realizing to invoke itself (and losing scope due prototype.call()
if(moduleArguments[0] == 'invoke') {
moduleArguments = Array.prototype.slice.call( moduleArguments, 1 );
}
return module.invoke(moduleArguments[0], this, Array.prototype.slice.call( moduleArguments, 1 ) );
}
// initializing
module.initialize();
})
;
return this;
};
$.fn.colorize.settings = {
name : 'Image Colorizer',
debug : true,
namespace : 'colorize',
onDraw : function(overlayContext, imageName, colorName, color) {},
// whether to block execution while updating canvas
async : true,
// object containing names and default values of color regions
colors : {},
metadata: {
image : 'image',
name : 'name'
},
error: {
noImage : 'No tracing image specified',
undefinedColors : 'No default colors specified.',
missingColor : 'Attempted to change color that does not exist',
missingPlugin : 'Blend onto plug-in must be included',
undefinedHeight : 'The width or height of image canvas could not be automatically determined. Please specify a height.'
}
};
})( jQuery, window , document );

@ -1,11 +0,0 @@
/*!
* # Semantic UI 2.0.0 - Colorize
* http://github.com/semantic-org/semantic-ui/
*
*
* Copyright 2015 Contributors
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/
!function(e,n,i,t){"use strict";e.fn.colorize=function(n){var i=e.isPlainObject(n)?e.extend(!0,{},e.fn.colorize.settings,n):e.extend({},e.fn.colorize.settings),o=arguments||!1;return e(this).each(function(n){var a,r,c,s,d,g,u,l,m=e(this),f=e("<canvas />")[0],h=e("<canvas />")[0],p=e("<canvas />")[0],v=new Image,w=i.colors,b=(i.paths,i.namespace),y=i.error,C=m.data("module-"+b);return l={checkPreconditions:function(){return l.debug("Checking pre-conditions"),!e.isPlainObject(w)||e.isEmptyObject(w)?(l.error(y.undefinedColors),!1):!0},async:function(e){i.async?setTimeout(e,0):e()},getMetadata:function(){l.debug("Grabbing metadata"),s=m.data("image")||i.image||t,d=m.data("name")||i.name||n,g=i.width||m.width(),u=i.height||m.height(),(0===g||0===u)&&l.error(y.undefinedSize)},initialize:function(){l.debug("Initializing with colors",w),l.checkPreconditions()&&l.async(function(){l.getMetadata(),l.canvas.create(),l.draw.image(function(){l.draw.colors(),l.canvas.merge()}),m.data("module-"+b,l)})},redraw:function(){l.debug("Redrawing image"),l.async(function(){l.canvas.clear(),l.draw.colors(),l.canvas.merge()})},change:{color:function(e,n){return l.debug("Changing color",e),w[e]===t?(l.error(y.missingColor),!1):(w[e]=n,void l.redraw())}},canvas:{create:function(){l.debug("Creating canvases"),f.width=g,f.height=u,h.width=g,h.height=u,p.width=g,p.height=u,a=f.getContext("2d"),r=h.getContext("2d"),c=p.getContext("2d"),m.append(f),a=m.children("canvas")[0].getContext("2d")},clear:function(e){l.debug("Clearing canvas"),c.fillStyle="#FFFFFF",c.fillRect(0,0,g,u)},merge:function(){return e.isFunction(a.blendOnto)?(a.putImageData(r.getImageData(0,0,g,u),0,0),void c.blendOnto(a,"multiply")):void l.error(y.missingPlugin)}},draw:{image:function(e){l.debug("Drawing image"),e=e||function(){},s?(v.src=s,v.onload=function(){r.drawImage(v,0,0),e()}):(l.error(y.noImage),e())},colors:function(){l.debug("Drawing color overlays",w),e.each(w,function(e,n){i.onDraw(c,d,e,n)})}},debug:function(e,n){i.debug&&(n!==t?console.info(i.name+": "+e,n):console.info(i.name+": "+e))},error:function(e){console.warn(i.name+": "+e)},invoke:function(n,o,a){var r;return a=a||Array.prototype.slice.call(arguments,2),"string"==typeof n&&C!==t&&(n=n.split("."),e.each(n,function(n,t){return e.isPlainObject(C[t])?(C=C[t],!0):e.isFunction(C[t])?(r=C[t],!0):(l.error(i.error.method),!1)})),e.isFunction(r)?r.apply(o,a):!1}},C!==t&&o?("invoke"==o[0]&&(o=Array.prototype.slice.call(o,1)),l.invoke(o[0],this,Array.prototype.slice.call(o,1))):void l.initialize()}),this},e.fn.colorize.settings={name:"Image Colorizer",debug:!0,namespace:"colorize",onDraw:function(e,n,i,t){},async:!0,colors:{},metadata:{image:"image",name:"name"},error:{noImage:"No tracing image specified",undefinedColors:"No default colors specified.",missingColor:"Attempted to change color that does not exist",missingPlugin:"Blend onto plug-in must be included",undefinedHeight:"The width or height of image canvas could not be automatically determined. Please specify a height."}}}(jQuery,window,document);

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Comment
* # Semantic UI 2.4.0 - Comment
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Comment
* # Semantic UI 2.4.0 - Comment
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Container
* # Semantic UI 2.4.0 - Container
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Container
* # Semantic UI 2.4.0 - Container
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Dimmer
* # Semantic UI 2.4.0 - Dimmer
* http://github.com/semantic-org/semantic-ui/
*
*
@ -115,6 +115,18 @@
*******************************/
/*--------------
Legacy
---------------*/
/* Animating / Active / Visible */
.dimmed.dimmable > .ui.animating.legacy.dimmer,
.dimmed.dimmable > .ui.visible.legacy.dimmer,
.ui.active.legacy.dimmer {
display: block;
}
/*--------------
Alignment
---------------*/

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Dimmer
* # Semantic UI 2.4.0 - Dimmer
* http://github.com/semantic-org/semantic-ui/
*
*
@ -83,7 +83,6 @@ $.fn.dimmer = function(parameters) {
else {
$dimmer = module.create();
}
module.set.variation();
}
},
@ -114,10 +113,6 @@ $.fn.dimmer = function(parameters) {
bind: {
events: function() {
if(module.is.page()) {
// touch events default to passive, due to changes in chrome to optimize mobile perf
$dimmable.get(0).addEventListener('touchmove', module.event.preventScroll, { passive: false });
}
if(settings.on == 'hover') {
$dimmable
.on('mouseenter' + eventNamespace, module.show)
@ -145,9 +140,6 @@ $.fn.dimmer = function(parameters) {
unbind: {
events: function() {
if(module.is.page()) {
$dimmable.get(0).removeEventListener('touchmove', module.event.preventScroll, { passive: false });
}
$module
.removeData(moduleNamespace)
;
@ -165,9 +157,6 @@ $.fn.dimmer = function(parameters) {
event.stopImmediatePropagation();
}
},
preventScroll: function(event) {
event.preventDefault();
}
},
addContent: function(element) {
@ -200,6 +189,7 @@ $.fn.dimmer = function(parameters) {
: function(){}
;
module.debug('Showing dimmer', $dimmer, settings);
module.set.variation();
if( (!module.is.dimmed() || module.is.animating()) && module.is.enabled() ) {
module.animate.show(callback);
settings.onShow.call(element);
@ -243,12 +233,22 @@ $.fn.dimmer = function(parameters) {
: function(){}
;
if(settings.useCSS && $.fn.transition !== undefined && $dimmer.transition('is supported')) {
if(settings.useFlex) {
module.debug('Using flex dimmer');
module.remove.legacy();
}
else {
module.debug('Using legacy non-flex dimmer');
module.set.legacy();
}
if(settings.opacity !== 'auto') {
module.set.opacity();
}
$dimmer
.transition({
displayType : 'flex',
displayType : settings.useFlex
? 'flex'
: 'block',
animation : settings.transition + ' in',
queue : false,
duration : module.get.duration(),
@ -293,7 +293,9 @@ $.fn.dimmer = function(parameters) {
module.verbose('Hiding dimmer with css');
$dimmer
.transition({
displayType : 'flex',
displayType : settings.useFlex
? 'flex'
: 'block',
animation : settings.transition + ' out',
queue : false,
duration : module.get.duration(),
@ -302,6 +304,7 @@ $.fn.dimmer = function(parameters) {
module.remove.dimmed();
},
onComplete : function() {
module.remove.variation();
module.remove.active();
callback();
}
@ -415,6 +418,9 @@ $.fn.dimmer = function(parameters) {
module.debug('Setting opacity to', opacity);
$dimmer.css('background-color', color);
},
legacy: function() {
$dimmer.addClass(className.legacy);
},
active: function() {
$dimmer.addClass(className.active);
},
@ -444,6 +450,9 @@ $.fn.dimmer = function(parameters) {
.removeClass(className.active)
;
},
legacy: function() {
$dimmer.removeClass(className.legacy);
},
dimmed: function() {
$dimmable.removeClass(className.dimmed);
},
@ -657,6 +666,9 @@ $.fn.dimmer.settings = {
verbose : false,
performance : true,
// whether should use flex layout
useFlex : true,
// name to distinguish between multiple dimmers in context
dimmerName : false,
@ -700,6 +712,7 @@ $.fn.dimmer.settings = {
dimmer : 'dimmer',
disabled : 'disabled',
hide : 'hide',
legacy : 'legacy',
pageDimmer : 'page',
show : 'show'
},

@ -1,9 +1,9 @@
/*!
* # Semantic UI 2.3.3 - Dimmer
* # Semantic UI 2.4.0 - Dimmer
* http://github.com/semantic-org/semantic-ui/
*
*
* Released under the MIT license
* http://opensource.org/licenses/MIT
*
*/.dimmable:not(body){position:relative}.ui.dimmer{display:none;position:absolute;top:0!important;left:0!important;width:100%;height:100%;text-align:center;vertical-align:middle;padding:1em;background-color:rgba(0,0,0,.85);opacity:0;line-height:1;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-transition:background-color .5s linear;transition:background-color .5s linear;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;will-change:opacity;z-index:1000}.ui.dimmer>.content{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;color:#fff}.ui.segment>.ui.dimmer{border-radius:inherit!important}.ui.dimmer:not(.inverted)::-webkit-scrollbar-track{background:rgba(255,255,255,.1)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb{background:rgba(255,255,255,.25)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:window-inactive{background:rgba(255,255,255,.15)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.35)}.animating.dimmable:not(body),.dimmed.dimmable:not(body){overflow:hidden}.dimmed.dimmable>.ui.animating.dimmer,.dimmed.dimmable>.ui.visible.dimmer,.ui.active.dimmer{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.ui.disabled.dimmer{width:0!important;height:0!important}.ui[class*="top aligned"].dimmer{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.ui[class*="bottom aligned"].dimmer{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.ui.page.dimmer{position:fixed;-webkit-transform-style:'';transform-style:'';-webkit-perspective:2000px;perspective:2000px;-webkit-transform-origin:center center;transform-origin:center center}body.animating.in.dimmable,body.dimmed.dimmable{overflow:hidden}body.dimmable>.dimmer{position:fixed}.blurring.dimmable>:not(.dimmer){-webkit-filter:blur(0) grayscale(0);filter:blur(0) grayscale(0);-webkit-transition:.8s -webkit-filter ease;transition:.8s -webkit-filter ease;transition:.8s filter ease;transition:.8s filter ease,.8s -webkit-filter ease}.blurring.dimmed.dimmable>:not(.dimmer){-webkit-filter:blur(5px) grayscale(.7);filter:blur(5px) grayscale(.7)}.blurring.dimmable>.dimmer{background-color:rgba(0,0,0,.6)}.blurring.dimmable>.inverted.dimmer{background-color:rgba(255,255,255,.6)}.ui.dimmer>.top.aligned.content>*{vertical-align:top}.ui.dimmer>.bottom.aligned.content>*{vertical-align:bottom}.ui.inverted.dimmer{background-color:rgba(255,255,255,.85)}.ui.inverted.dimmer>.content>*{color:#fff}.ui.simple.dimmer{display:block;overflow:hidden;opacity:1;width:0%;height:0%;z-index:-100;background-color:rgba(0,0,0,0)}.dimmed.dimmable>.ui.simple.dimmer{overflow:visible;opacity:1;width:100%;height:100%;background-color:rgba(0,0,0,.85);z-index:1}.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,0)}.dimmed.dimmable>.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,.85)}
*/.dimmable:not(body){position:relative}.ui.dimmer{display:none;position:absolute;top:0!important;left:0!important;width:100%;height:100%;text-align:center;vertical-align:middle;padding:1em;background-color:rgba(0,0,0,.85);opacity:0;line-height:1;-webkit-animation-fill-mode:both;animation-fill-mode:both;-webkit-animation-duration:.5s;animation-duration:.5s;-webkit-transition:background-color .5s linear;transition:background-color .5s linear;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;will-change:opacity;z-index:1000}.ui.dimmer>.content{-webkit-user-select:text;-moz-user-select:text;-ms-user-select:text;user-select:text;color:#fff}.ui.segment>.ui.dimmer{border-radius:inherit!important}.ui.dimmer:not(.inverted)::-webkit-scrollbar-track{background:rgba(255,255,255,.1)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb{background:rgba(255,255,255,.25)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:window-inactive{background:rgba(255,255,255,.15)}.ui.dimmer:not(.inverted)::-webkit-scrollbar-thumb:hover{background:rgba(255,255,255,.35)}.animating.dimmable:not(body),.dimmed.dimmable:not(body){overflow:hidden}.dimmed.dimmable>.ui.animating.dimmer,.dimmed.dimmable>.ui.visible.dimmer,.ui.active.dimmer{display:-webkit-box;display:-ms-flexbox;display:flex;opacity:1}.ui.disabled.dimmer{width:0!important;height:0!important}.dimmed.dimmable>.ui.animating.legacy.dimmer,.dimmed.dimmable>.ui.visible.legacy.dimmer,.ui.active.legacy.dimmer{display:block}.ui[class*="top aligned"].dimmer{-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start}.ui[class*="bottom aligned"].dimmer{-webkit-box-pack:end;-ms-flex-pack:end;justify-content:flex-end}.ui.page.dimmer{position:fixed;-webkit-transform-style:'';transform-style:'';-webkit-perspective:2000px;perspective:2000px;-webkit-transform-origin:center center;transform-origin:center center}body.animating.in.dimmable,body.dimmed.dimmable{overflow:hidden}body.dimmable>.dimmer{position:fixed}.blurring.dimmable>:not(.dimmer){-webkit-filter:blur(0) grayscale(0);filter:blur(0) grayscale(0);-webkit-transition:.8s -webkit-filter ease;transition:.8s -webkit-filter ease;transition:.8s filter ease;transition:.8s filter ease,.8s -webkit-filter ease}.blurring.dimmed.dimmable>:not(.dimmer){-webkit-filter:blur(5px) grayscale(.7);filter:blur(5px) grayscale(.7)}.blurring.dimmable>.dimmer{background-color:rgba(0,0,0,.6)}.blurring.dimmable>.inverted.dimmer{background-color:rgba(255,255,255,.6)}.ui.dimmer>.top.aligned.content>*{vertical-align:top}.ui.dimmer>.bottom.aligned.content>*{vertical-align:bottom}.ui.inverted.dimmer{background-color:rgba(255,255,255,.85)}.ui.inverted.dimmer>.content>*{color:#fff}.ui.simple.dimmer{display:block;overflow:hidden;opacity:1;width:0%;height:0%;z-index:-100;background-color:rgba(0,0,0,0)}.dimmed.dimmable>.ui.simple.dimmer{overflow:visible;opacity:1;width:100%;height:100%;background-color:rgba(0,0,0,.85);z-index:1}.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,0)}.dimmed.dimmable>.ui.simple.inverted.dimmer{background-color:rgba(255,255,255,.85)}

File diff suppressed because one or more lines are too long

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Divider
* # Semantic UI 2.4.0 - Divider
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Divider
* # Semantic UI 2.4.0 - Divider
* http://github.com/semantic-org/semantic-ui/
*
*

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Dropdown
* # Semantic UI 2.4.0 - Dropdown
* http://github.com/semantic-org/semantic-ui/
*
*
@ -519,7 +519,7 @@ select.ui.dropdown {
/* Dropdown Icon */
.ui.active.selection.dropdown > .dropdown.icon,
.ui.visible.selection.dropdown > .dropdown.icon {
opacity: 1;
opacity: '';
z-index: 3;
}
@ -735,7 +735,7 @@ select.ui.dropdown {
color: inherit;
}
.ui.inline.dropdown .dropdown.icon {
margin: 0em 0.5em 0em 0.21428571em;
margin: 0em 0.21428571em 0em 0.21428571em;
vertical-align: baseline;
}
.ui.inline.dropdown > .text {
@ -946,6 +946,19 @@ select.ui.dropdown {
background-color: #FDCFCF;
}
/*--------------------
Clear
----------------------*/
.ui.dropdown > .clear.dropdown.icon {
opacity: 0.8;
-webkit-transition: opacity 0.1s ease;
transition: opacity 0.1s ease;
}
.ui.dropdown > .clear.dropdown.icon:hover {
opacity: 1;
}
/*--------------------
Disabled
----------------------*/
@ -1450,7 +1463,7 @@ select.ui.dropdown {
/* Dropdown Carets */
@font-face {
font-family: 'Dropdown';
src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMggjB5AAAAC8AAAAYGNtYXAPfuIIAAABHAAAAExnYXNwAAAAEAAAAWgAAAAIZ2x5Zjo82LgAAAFwAAABVGhlYWQAQ88bAAACxAAAADZoaGVhAwcB6QAAAvwAAAAkaG10eAS4ABIAAAMgAAAAIGxvY2EBNgDeAAADQAAAABJtYXhwAAoAFgAAA1QAAAAgbmFtZVcZpu4AAAN0AAABRXBvc3QAAwAAAAAEvAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADw2gHg/+D/4AHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEADgAAAAKAAgAAgACAAEAIPDa//3//wAAAAAAIPDX//3//wAB/+MPLQADAAEAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAIABJQElABMAABM0NzY3BTYXFhUUDwEGJwYvASY1AAUGBwEACAUGBoAFCAcGgAUBEgcGBQEBAQcECQYHfwYBAQZ/BwYAAQAAAG4BJQESABMAADc0PwE2MzIfARYVFAcGIyEiJyY1AAWABgcIBYAGBgUI/wAHBgWABwaABQWABgcHBgUFBgcAAAABABIASQC3AW4AEwAANzQ/ATYXNhcWHQEUBwYnBi8BJjUSBoAFCAcFBgYFBwgFgAbbBwZ/BwEBBwQJ/wgEBwEBB38GBgAAAAABAAAASQClAW4AEwAANxE0NzYzMh8BFhUUDwEGIyInJjUABQYHCAWABgaABQgHBgVbAQAIBQYGgAUIBwWABgYFBwAAAAEAAAABAADZuaKOXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAAAAACgAUAB4AQgBkAIgAqgAAAAEAAAAIABQAAQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAOAAAAAQAAAAAAAgAOAEcAAQAAAAAAAwAOACQAAQAAAAAABAAOAFUAAQAAAAAABQAWAA4AAQAAAAAABgAHADIAAQAAAAAACgA0AGMAAwABBAkAAQAOAAAAAwABBAkAAgAOAEcAAwABBAkAAwAOACQAAwABBAkABAAOAFUAAwABBAkABQAWAA4AAwABBAkABgAOADkAAwABBAkACgA0AGMAaQBjAG8AbQBvAG8AbgBWAGUAcgBzAGkAbwBuACAAMQAuADAAaQBjAG8AbQBvAG8Abmljb21vb24AaQBjAG8AbQBvAG8AbgBSAGUAZwB1AGwAYQByAGkAYwBvAG0AbwBvAG4ARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=) format('truetype'), url(data:application/font-woff;charset=utf-8;base64,d09GRk9UVE8AAAVwAAoAAAAABSgAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABDRkYgAAAA9AAAAdkAAAHZLDXE/09TLzIAAALQAAAAYAAAAGAIIweQY21hcAAAAzAAAABMAAAATA9+4ghnYXNwAAADfAAAAAgAAAAIAAAAEGhlYWQAAAOEAAAANgAAADYAQ88baGhlYQAAA7wAAAAkAAAAJAMHAelobXR4AAAD4AAAACAAAAAgBLgAEm1heHAAAAQAAAAABgAAAAYACFAAbmFtZQAABAgAAAFFAAABRVcZpu5wb3N0AAAFUAAAACAAAAAgAAMAAAEABAQAAQEBCGljb21vb24AAQIAAQA6+BwC+BsD+BgEHgoAGVP/i4seCgAZU/+LiwwHi2v4lPh0BR0AAACIDx0AAACNER0AAAAJHQAAAdASAAkBAQgPERMWGyAlKmljb21vb25pY29tb29udTB1MXUyMHVGMEQ3dUYwRDh1RjBEOXVGMERBAAACAYkABgAIAgABAAQABwAKAA0AVgCfAOgBL/yUDvyUDvyUDvuUDvtvi/emFYuQjZCOjo+Pj42Qiwj3lIsFkIuQiY6Hj4iNhouGi4aJh4eHCPsU+xQFiIiGiYaLhouHjYeOCPsU9xQFiI+Jj4uQCA77b4v3FBWLkI2Pjo8I9xT3FAWPjo+NkIuQi5CJjogI9xT7FAWPh42Hi4aLhomHh4eIiIaJhosI+5SLBYaLh42HjoiPiY+LkAgO+92d928Vi5CNkI+OCPcU9xQFjo+QjZCLkIuPiY6Hj4iNhouGCIv7lAWLhomHh4iIh4eJhouGi4aNiI8I+xT3FAWHjomPi5AIDvvdi+YVi/eUBYuQjZCOjo+Pj42Qi5CLkImOhwj3FPsUBY+IjYaLhouGiYeHiAj7FPsUBYiHhomGi4aLh42Hj4iOiY+LkAgO+JQU+JQViwwKAAAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA8NoB4P/g/+AB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABAA4AAAACgAIAAIAAgABACDw2v/9//8AAAAAACDw1//9//8AAf/jDy0AAwABAAAAAAAAAAAAAAABAAH//wAPAAEAAAABAAA5emozXw889QALAgAAAAAA0ABHWAAAAADQAEdYAAAAAAElAW4AAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAASUAAQAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAABAAAAASUAAAElAAAAtwASALcAAAAAUAAACAAAAAAADgCuAAEAAAAAAAEADgAAAAEAAAAAAAIADgBHAAEAAAAAAAMADgAkAAEAAAAAAAQADgBVAAEAAAAAAAUAFgAOAAEAAAAAAAYABwAyAAEAAAAAAAoANABjAAMAAQQJAAEADgAAAAMAAQQJAAIADgBHAAMAAQQJAAMADgAkAAMAAQQJAAQADgBVAAMAAQQJAAUAFgAOAAMAAQQJAAYADgA5AAMAAQQJAAoANABjAGkAYwBvAG0AbwBvAG4AVgBlAHIAcwBpAG8AbgAgADEALgAwAGkAYwBvAG0AbwBvAG5pY29tb29uAGkAYwBvAG0AbwBvAG4AUgBlAGcAdQBsAGEAcgBpAGMAbwBtAG8AbwBuAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('woff');
src: url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAVgAA8AAAAACFAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABGRlRNAAABWAAAABwAAAAchGgaq0dERUYAAAF0AAAAHAAAAB4AJwAPT1MvMgAAAZAAAABDAAAAVnW4TJdjbWFwAAAB1AAAAEsAAAFS8CcaqmN2dCAAAAIgAAAABAAAAAQAEQFEZ2FzcAAAAiQAAAAIAAAACP//AANnbHlmAAACLAAAAQoAAAGkrRHP9WhlYWQAAAM4AAAAMAAAADYPK8YyaGhlYQAAA2gAAAAdAAAAJANCAb1obXR4AAADiAAAACIAAAAiCBkAOGxvY2EAAAOsAAAAFAAAABQBnAIybWF4cAAAA8AAAAAfAAAAIAEVAF5uYW1lAAAD4AAAATAAAAKMFGlj5HBvc3QAAAUQAAAARgAAAHJoedjqd2ViZgAABVgAAAAGAAAABrO7W5UAAAABAAAAANXulPUAAAAA1r4hgAAAAADXu2Q1eNpjYGRgYOABYjEgZmJgBEIOIGYB8xgAA/YAN3jaY2BktGOcwMDKwMI4jTGNgYHBHUp/ZZBkaGFgYGJgZWbACgLSXFMYHFT/fLjFeOD/AQY9xjMMbkBhRpAcAN48DQYAeNpjYGBgZoBgGQZGBhDwAfIYwXwWBgMgzQGETAwMqn8+8H649f8/lHX9//9b7Pzf+fWgusCAkY0BzmUE6gHpQwGMDMMeAACbxg7SAAARAUQAAAAB//8AAnjadZBPSsNAGMXfS+yMqYgOhpSuSlKadmUhiVEhEMQzFF22m17BbbvzCh5BXCUn6EG8gjeQ4DepwYo4i+/ffL95j4EDA+CFC7jQuKyIeVHrI3wkleq9F7XrSInKteOeHdda8bOoaeepSc00NWPz/LRec9G8GabyGtEdF7h19z033GAMTK7zbM42xNEZpzYof0RtQ5CUHAQJ73OtVyutc+3b7Ou//b8XNlsPx3jgjUifABdhEohKJJL5iM5p39uqc7X1+sRQSqmGrUVhlsJ4lpmEUVwyT8SUYtg0P9DyNzPADDs+tjrGV6KRCRfsui3eHcL4/p8ZXvfMlcnEU+CLv7hDykOP+AKTPTxbAAB42mNgZGBgAGKuf5KP4vltvjLIMzGAwLV9ig0g+vruFFMQzdjACOJzMIClARh0CTJ42mNgZGBgPPD/AJD8wgAEjA0MjAyogAMAbOQEAQAAAAC7ABEAAAAAAKoAAAH0AAABgAAAAUAACAFAAAgAwAAXAAAAAAAAACoAKgAqADIAbACGAKAAugDSeNpjYGRgYOBkUGFgYgABEMkFhAwM/xn0QAIADdUBdAB42qWQvUoDQRSFv3GjaISUQaymSmGxJoGAsRC0iPYLsU50Y6IxrvlRtPCJJKUPIBb+PIHv4EN4djKuKAqCDHfmu+feOdwZoMCUAJNbAlYUMzaUlM14jjxbngOq7HnOia89z1Pk1vMCa9x7ztPkzfMyJbPj+ZGi6Xp+omxuPD+zaD7meaFg7mb8GrBqHmhwxoAxlm0uiRkpP9X5m26pKRoMxTGR1D49Dv/Yb/91o6l8qL6eu5n2hZQzn68utR9m3FU2cB4t9cdSLG2utI+44Eh/P9bqKO+oJ/WxmXssj77YkrjasZQD6SFddythk3Wtzrf+UF2p076Udla1VNzsERP3kkjVRKel7mp1udXYcHtZSlV7RfmJe1GiFWveluaeKD5/MuJcSk8Tpm/vvwPIbmJleNpjYGKAAFYG7ICTgYGRiZGZkYWRlZGNkZ2Rg5GTLT2nsiDDEEIZsZfmZRqZujmDaDcDAxcI7WIOpS2gtCWUdgQAZkcSmQAAAAFblbO6AAA=) format('woff');
font-weight: normal;
font-style: normal;
}
@ -1485,19 +1498,15 @@ select.ui.dropdown {
.ui.vertical.menu .dropdown.item > .dropdown.icon:before {
content: "\f0da" /*rtl:"\f0d9"*/;
}
/* Icons for Reference
.dropdown.down.icon {
content: "\f0d7";
}
.dropdown.up.icon {
content: "\f0d8";
}
.dropdown.left.icon {
content: "\f0d9";
}
.dropdown.icon.icon {
content: "\f0da";
.ui.dropdown > .clear.icon:before {
content: "\f00d";
}
/* Icons for Reference (Subsetted in 2.4.0)
.dropdown.down:before { content: "\f0d7"; }
.dropdown.up:before { content: "\f0d8"; }
.dropdown.left:before { content: "\f0d9"; }
.dropdown.right:before { content: "\f0da"; }
.dropdown.close:before { content: "\f00d"; }
*/

@ -1,5 +1,5 @@
/*!
* # Semantic UI 2.3.3 - Dropdown
* # Semantic UI 2.4.0 - Dropdown
* http://github.com/semantic-org/semantic-ui/
*
*
@ -1019,7 +1019,12 @@ $.fn.dropdown = function(parameters) {
},
icon: {
click: function(event) {
module.toggle();
if($icon.hasClass(className.clear)) {
module.clear();
}
else {
module.toggle();
}
}
},
text: {
@ -1646,7 +1651,7 @@ $.fn.dropdown = function(parameters) {
},
hide: function(text, value, element) {
module.set.value(value, text);
module.set.value(value, text, $(element));
module.hideAndClear();
}
@ -2481,6 +2486,15 @@ $.fn.dropdown = function(parameters) {
$module.data(metadata.value, stringValue);
}
}
if(module.is.single() && settings.clearable) {
// treat undefined or '' as empty
if(!escapedValue) {
module.remove.clearable();
}
else {
module.set.clearable();
}
}
if(settings.fireOnInit === false && module.is.initialLoad()) {
module.verbose('No callback on initial load', settings.onChange);
}
@ -2576,7 +2590,10 @@ $.fn.dropdown = function(parameters) {
}
})
;
}
},
clearable: function() {
$icon.addClass(className.clear);
},
},
add: {
@ -2774,7 +2791,7 @@ $.fn.dropdown = function(parameters) {
}
module.set.value(newValue, addedValue, addedText, $selectedItem);
module.check.maxSelections();
}
},
},
remove: {
@ -2999,6 +3016,9 @@ $.fn.dropdown = function(parameters) {
.removeAttr('tabindex')
;
}
},
clearable: function() {
$icon.removeClass(className.clear);
}
},
@ -3686,6 +3706,8 @@ $.fn.dropdown.settings = {
values : false, // specify values to use for dropdown
clearable : false, // whether the value of the dropdown can be cleared
apiSettings : false,
selectOnKeydown : true, // Whether selection should occur automatically when keyboard shortcuts used
minCharacters : 0, // Minimum characters required to trigger API call
@ -3838,6 +3860,7 @@ $.fn.dropdown.settings = {
active : 'active',
addition : 'addition',
animating : 'animating',
clear : 'clear',
disabled : 'disabled',
empty : 'empty',
dropdown : 'ui dropdown',

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save