mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-23 22:27:17 -04:00
Merge branch 'development' into daemon
# Conflicts: # bazarr.py # bazarr/get_languages.py
This commit is contained in:
commit
3e17afc028
218 changed files with 7054 additions and 10094 deletions
|
@ -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)
|
||||
# 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()
|
||||
|
||||
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)
|
||||
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:
|
||||
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)
|
||||
message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode(calculated_score) + "% using filename guessing."
|
||||
|
||||
return message
|
||||
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, err = process.communicate()
|
||||
|
||||
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:
|
||||
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
|
||||
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 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."
|
||||
|
||||
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 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()
|
||||
out, err = process.communicate()
|
||||
|
||||
process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
# wait for the process to terminate
|
||||
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
|
||||
|
||||
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())
|
||||
rebulk.rebulk(groups())
|
||||
|
||||
rebulk.rebulk(path(_config('path')))
|
||||
rebulk.rebulk(groups(_config('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(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())
|
||||
|
||||
rebulk.rebulk(processors(_config('processors')))
|
||||
|
||||
rebulk.rebulk(mimetype(_config('mimetype')))
|
||||
rebulk.rebulk(type_(_config('type')))
|
||||
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.regex("True-?HD", value="TrueHD")
|
||||
|
||||
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_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", 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.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 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
|
||||
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
|
||||
|
||||
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,40 +57,39 @@ 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']
|
||||
discrete_separators = strong_discrete_separators + weak_discrete_separators
|
||||
season_episode_seps = []
|
||||
season_episode_seps.extend(seps)
|
||||
season_episode_seps.extend(['x', 'X', 'e', 'E'])
|
||||
|
||||
max_range_gap = config['max_range_gap']
|
||||
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
|
||||
|
||||
def ordering_validator(match):
|
||||
"""
|
||||
|
@ -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)
|
||||
|
|
72
libs/guessit/rules/properties/format.py
Normal file
72
libs/guessit/rules/properties/format.py
Normal file
|
@ -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')
|
||||
tags=['other.validate.mux', 'video-codec-prefix', 'format-suffix'])
|
||||
rebulk.string('HC', value='Hardcoded Subtitles')
|
||||
|
||||
rebulk.string('SDR', value='Standard Dynamic Range', tags='uhdbluray-neighbor')
|
||||
rebulk.regex('HDR(?:10)?', value='HDR10', tags='uhdbluray-neighbor')
|
||||
rebulk.regex('Dolby-?Vision', value='Dolby Vision', tags='uhdbluray-neighbor')
|
||||
rebulk.regex('BT-?2020', value='BT.2020', tags='uhdbluray-neighbor')
|
||||
|
||||
rebulk.string('Sample', value='Sample', tags=['at-end', 'not-a-release-group'])
|
||||
rebulk.string('Proof', value='Proof', tags=['at-end', 'not-a-release-group'])
|
||||
rebulk.string('Obfuscated', 'Scrambled', value='Obfuscated', tags=['at-end', 'not-a-release-group'])
|
||||
rebulk.string('xpost', 'postbot', 'asrequested', value='Repost', tags='not-a-release-group')
|
||||
|
||||
rebulk.rules(RenameAnotherToOther, ValidateHasNeighbor, ValidateHasNeighborAfter, ValidateHasNeighborBefore,
|
||||
ValidateScreenerRule, ValidateMuxRule, ValidateHardcodedSubs, ValidateStreamingServiceNeighbor,
|
||||
ValidateAtEnd, ProperCountRule)
|
||||
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',
|
||||
forbidden_groupnames = ['rip', 'by', 'for', 'par', 'pour', 'bonus']
|
||||
|
||||
groupname_ignore_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
|
||||
|
||||
|
||||
_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')
|
||||
'subtitle_language.suffix', 'subtitle_language.prefix', 'language.suffix']
|
||||
|
||||
_scene_previous_tags = ('release-group-prefix', )
|
||||
|
||||
|
||||
class DashSeparatedReleaseGroup(Rule):
|
||||
"""
|
||||
Detect dash separated release groups that might appear at the end or at the beginning of a release name.
|
||||
|
||||
Series.S01E02.Pilot.DVDRip.x264-CS.mkv
|
||||
release_group: CS
|
||||
abc-the.title.name.1983.1080p.bluray.x264.mkv
|
||||
release_group: abc
|
||||
|
||||
At the end: Release groups should be dash-separated and shouldn't contain spaces nor
|
||||
appear in a group with other matches. The preceding matches should be separated by dot.
|
||||
If a release group is found, the conflicting matches are removed.
|
||||
|
||||
At the beginning: Release groups should be dash-separated and shouldn't contain spaces nor appear in a group.
|
||||
It should be followed by a hole with dot-separated words.
|
||||
Detection only happens if no matches exist at the beginning.
|
||||
"""
|
||||
consequence = [RemoveMatch, AppendMatch]
|
||||
|
||||
def __init__(self, value_formatter):
|
||||
"""Default constructor."""
|
||||
super(DashSeparatedReleaseGroup, self).__init__()
|
||||
self.value_formatter = value_formatter
|
||||
|
||||
@classmethod
|
||||
def is_valid(cls, matches, candidate, start, end, at_end): # pylint:disable=inconsistent-return-statements
|
||||
"""
|
||||
Whether a candidate is a valid release group.
|
||||
"""
|
||||
if not at_end:
|
||||
if len(candidate.value) <= 1:
|
||||
return False
|
||||
|
||||
if matches.markers.at_match(candidate, predicate=lambda m: m.name == 'group'):
|
||||
return False
|
||||
|
||||
first_hole = matches.holes(candidate.end, end, predicate=lambda m: m.start == candidate.end, index=0)
|
||||
if not first_hole:
|
||||
return False
|
||||
|
||||
raw_value = first_hole.raw
|
||||
return raw_value[0] == '-' and '-' not in raw_value[1:] and '.' in raw_value and ' ' not in raw_value
|
||||
|
||||
group = matches.markers.at_match(candidate, predicate=lambda m: m.name == 'group', index=0)
|
||||
if group and matches.at_match(group, predicate=lambda m: not m.private and m.span != candidate.span):
|
||||
return False
|
||||
|
||||
count = 0
|
||||
match = candidate
|
||||
while match:
|
||||
current = matches.range(start, match.start, index=-1, predicate=lambda m: not m.private)
|
||||
if not current:
|
||||
break
|
||||
|
||||
separator = match.input_string[current.end:match.start]
|
||||
if not separator and match.raw[0] == '-':
|
||||
separator = '-'
|
||||
|
||||
match = current
|
||||
|
||||
if count == 0:
|
||||
if separator != '-':
|
||||
break
|
||||
|
||||
count += 1
|
||||
continue
|
||||
|
||||
if separator == '.':
|
||||
return True
|
||||
|
||||
def detect(self, matches, start, end, at_end): # pylint:disable=inconsistent-return-statements
|
||||
"""
|
||||
Detect release group at the end or at the beginning of a filepart.
|
||||
"""
|
||||
candidate = None
|
||||
if at_end:
|
||||
container = matches.ending(end, lambda m: m.name == 'container', index=0)
|
||||
if container:
|
||||
end = container.start
|
||||
|
||||
candidate = matches.ending(end, index=0, predicate=(
|
||||
lambda m: not m.private and not (
|
||||
m.name == 'other' and 'not-a-release-group' in m.tags
|
||||
) and '-' not in m.raw and m.raw.strip() == m.raw))
|
||||
|
||||
if not candidate:
|
||||
if at_end:
|
||||
candidate = matches.holes(start, end, seps=seps, index=-1,
|
||||
predicate=lambda m: m.end == end and m.raw.strip(seps) and m.raw[0] == '-')
|
||||
else:
|
||||
candidate = matches.holes(start, end, seps=seps, index=0,
|
||||
predicate=lambda m: m.start == start and m.raw.strip(seps))
|
||||
|
||||
if candidate and self.is_valid(matches, candidate, start, end, at_end):
|
||||
return candidate
|
||||
|
||||
def when(self, matches, context): # pylint:disable=inconsistent-return-statements
|
||||
if matches.named('release_group'):
|
||||
return
|
||||
|
||||
to_remove = []
|
||||
to_append = []
|
||||
for filepart in matches.markers.named('path'):
|
||||
candidate = self.detect(matches, filepart.start, filepart.end, True)
|
||||
if candidate:
|
||||
to_remove.extend(matches.at_match(candidate))
|
||||
else:
|
||||
candidate = self.detect(matches, filepart.start, filepart.end, False)
|
||||
|
||||
if candidate:
|
||||
releasegroup = Match(candidate.start, candidate.end, name='release_group',
|
||||
formatter=self.value_formatter, input_string=candidate.input_string)
|
||||
|
||||
to_append.append(releasegroup)
|
||||
return to_remove, to_append
|
||||
_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']
|
||||
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()
|
||||
rebulk = rebulk.string_defaults(ignore_case=True).regex_defaults(flags=re.IGNORECASE)
|
||||
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.defaults(name='screen_size', validator=seps_surround, abbreviations=[dash],
|
||||
disabled=lambda context: is_disabled(context, 'screen_size'))
|
||||
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')
|
||||
|
||||
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')
|
||||
_digits_re = re.compile(r'\d+')
|
||||
|
||||
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})',
|
||||
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 = 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='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.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.defaults(name="video_profile", validator=seps_surround)
|
||||
|
||||
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.regex('10.?bits?', 'Hi10P?', 'YUV420P10', value='10bit')
|
||||
rebulk.regex('8.?bits?', value='8bit')
|
||||
|
||||
rebulk.string('DXVA', value='DXVA', name='video_api',
|
||||
disabled=lambda context: is_disabled(context, 'video_api'))
|
||||
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.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.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
|
138
libs/guessit/test/rules/format.yml
Normal file
138
libs/guessit/test/rules/format.yml
Normal file
|
@ -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
|
||||
? +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
|
||||
? "+360"
|
||||
? +500x360
|
||||
: screen_size: 360p
|
||||
|
||||
? +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
|
||||
? "+1080"
|
||||
? +500x1080
|
||||
: screen_size: 1080p
|
||||
|
||||
? +1440x1080
|
||||
: screen_size: 1080p
|
||||
aspect_ratio: 1.333
|
||||
|
||||
? +1920x1080
|
||||
: screen_size: 1080p
|
||||
aspect_ratio: 1.778
|
||||
|
||||
? +2048x1080
|
||||
: screen_size: 1080p
|
||||
aspect_ratio: 1.896
|
||||
|
||||
? +1080i
|
||||
? -1080p
|
||||
: screen_size: 1080i
|
||||
|
||||
? 1440p
|
||||
: screen_size: 1440p
|
||||
|
||||
? +500x1080
|
||||
: screen_size: 500x1080
|
||||
aspect_ratio: 0.463
|
||||
|
||||
? +2160p
|
||||
? +2160px
|
||||
? -2160i
|
||||
? -2160
|
||||
? +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:
|
||||
|
|
22
static/semantic/LICENSE
vendored
22
static/semantic/LICENSE
vendored
|
@ -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.
|
||||
|
7
static/semantic/README.md
vendored
7
static/semantic/README.md
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/accordion.min.css
vendored
2
static/semantic/components/accordion.min.css
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/ad.min.css
vendored
2
static/semantic/components/ad.min.css
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/button.min.css
vendored
2
static/semantic/components/button.min.css
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/card.min.css
vendored
2
static/semantic/components/card.min.css
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/checkbox.min.css
vendored
2
static/semantic/components/checkbox.min.css
vendored
|
@ -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 );
|
11
static/semantic/components/colorize.min.js
vendored
11
static/semantic/components/colorize.min.js
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/comment.min.css
vendored
2
static/semantic/components/comment.min.css
vendored
|
@ -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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/container.min.css
vendored
2
static/semantic/components/container.min.css
vendored
|
@ -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'
|
||||
},
|
||||
|
|
4
static/semantic/components/dimmer.min.css
vendored
4
static/semantic/components/dimmer.min.css
vendored
|
@ -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)}
|
2
static/semantic/components/dimmer.min.js
vendored
2
static/semantic/components/dimmer.min.js
vendored
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/
|
||||
*
|
||||
*
|
||||
|
|
2
static/semantic/components/divider.min.css
vendored
2
static/semantic/components/divider.min.css
vendored
|
@ -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',
|
||||
|
|
4
static/semantic/components/dropdown.min.css
vendored
4
static/semantic/components/dropdown.min.css
vendored
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…
Add table
Add a link
Reference in a new issue