|
|
|
#!/usr/bin/env python
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
|
|
Options
|
|
|
|
"""
|
|
|
|
import copy
|
|
|
|
import json
|
|
|
|
import os
|
|
|
|
import shlex
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
|
|
|
|
try:
|
|
|
|
from importlib.resources import read_text
|
|
|
|
except ImportError:
|
|
|
|
from importlib_resources import read_text
|
|
|
|
|
|
|
|
|
|
|
|
def build_argument_parser():
|
|
|
|
"""
|
|
|
|
Builds the argument parser
|
|
|
|
:return: the argument parser
|
|
|
|
:rtype: ArgumentParser
|
|
|
|
"""
|
|
|
|
opts = ArgumentParser()
|
|
|
|
opts.add_argument(dest='filename', help='Filename or release name to guess', nargs='*')
|
|
|
|
|
|
|
|
naming_opts = opts.add_argument_group("Naming")
|
|
|
|
naming_opts.add_argument('-t', '--type', dest='type', default=None,
|
|
|
|
help='The suggested file type: movie, episode. If undefined, type will be guessed.')
|
|
|
|
naming_opts.add_argument('-n', '--name-only', dest='name_only', action='store_true', default=None,
|
|
|
|
help='Parse files as name only, considering "/" and "\\" like other separators.')
|
|
|
|
naming_opts.add_argument('-Y', '--date-year-first', action='store_true', dest='date_year_first', default=None,
|
|
|
|
help='If short date is found, consider the first digits as the year.')
|
|
|
|
naming_opts.add_argument('-D', '--date-day-first', action='store_true', dest='date_day_first', default=None,
|
|
|
|
help='If short date is found, consider the second digits as the day.')
|
|
|
|
naming_opts.add_argument('-L', '--allowed-languages', action='append', dest='allowed_languages', default=None,
|
|
|
|
help='Allowed language (can be used multiple times)')
|
|
|
|
naming_opts.add_argument('-C', '--allowed-countries', action='append', dest='allowed_countries', default=None,
|
|
|
|
help='Allowed country (can be used multiple times)')
|
|
|
|
naming_opts.add_argument('-E', '--episode-prefer-number', action='store_true', dest='episode_prefer_number',
|
|
|
|
default=None,
|
|
|
|
help='Guess "serie.213.avi" as the episode 213. Without this option, '
|
|
|
|
'it will be guessed as season 2, episode 13')
|
|
|
|
naming_opts.add_argument('-T', '--expected-title', action='append', dest='expected_title', default=None,
|
|
|
|
help='Expected title to parse (can be used multiple times)')
|
|
|
|
naming_opts.add_argument('-G', '--expected-group', action='append', dest='expected_group', default=None,
|
|
|
|
help='Expected release group (can be used multiple times)')
|
|
|
|
naming_opts.add_argument('--includes', action='append', dest='includes', default=None,
|
|
|
|
help='List of properties to be detected')
|
|
|
|
naming_opts.add_argument('--excludes', action='append', dest='excludes', default=None,
|
|
|
|
help='List of properties to be ignored')
|
|
|
|
|
|
|
|
input_opts = opts.add_argument_group("Input")
|
|
|
|
input_opts.add_argument('-f', '--input-file', dest='input_file', default=None,
|
|
|
|
help='Read filenames from an input text file. File should use UTF-8 charset.')
|
|
|
|
|
|
|
|
output_opts = opts.add_argument_group("Output")
|
|
|
|
output_opts.add_argument('-v', '--verbose', action='store_true', dest='verbose', default=None,
|
|
|
|
help='Display debug output')
|
|
|
|
output_opts.add_argument('-P', '--show-property', dest='show_property', default=None,
|
|
|
|
help='Display the value of a single property (title, series, video_codec, year, ...)')
|
|
|
|
output_opts.add_argument('-a', '--advanced', dest='advanced', action='store_true', default=None,
|
|
|
|
help='Display advanced information for filename guesses, as json output')
|
|
|
|
output_opts.add_argument('-s', '--single-value', dest='single_value', action='store_true', default=None,
|
|
|
|
help='Keep only first value found for each property')
|
|
|
|
output_opts.add_argument('-l', '--enforce-list', dest='enforce_list', action='store_true', default=None,
|
|
|
|
help='Wrap each found value in a list even when property has a single value')
|
|
|
|
output_opts.add_argument('-j', '--json', dest='json', action='store_true', default=None,
|
|
|
|
help='Display information for filename guesses as json output')
|
|
|
|
output_opts.add_argument('-y', '--yaml', dest='yaml', action='store_true', default=None,
|
|
|
|
help='Display information for filename guesses as yaml output')
|
|
|
|
output_opts.add_argument('-i', '--output-input-string', dest='output_input_string', action='store_true',
|
|
|
|
default=False, help='Add input_string property in the output')
|
|
|
|
|
|
|
|
conf_opts = opts.add_argument_group("Configuration")
|
|
|
|
conf_opts.add_argument('-c', '--config', dest='config', action='append', default=None,
|
|
|
|
help='Filepath to configuration file. Configuration file contains the same '
|
|
|
|
'options as those from command line options, but option names have "-" characters '
|
|
|
|
'replaced with "_". This configuration will be merged with default and user '
|
|
|
|
'configuration files.')
|
|
|
|
conf_opts.add_argument('--no-user-config', dest='no_user_config', action='store_true',
|
|
|
|
default=None,
|
|
|
|
help='Disable user configuration. If not defined, guessit tries to read configuration files '
|
|
|
|
'at ~/.guessit/options.(json|yml|yaml) and ~/.config/guessit/options.(json|yml|yaml)')
|
|
|
|
conf_opts.add_argument('--no-default-config', dest='no_default_config', action='store_true',
|
|
|
|
default=None,
|
|
|
|
help='Disable default configuration. This should be done only if you are providing a full '
|
|
|
|
'configuration through user configuration or --config option. If no "advanced_config" '
|
|
|
|
'is provided by another configuration file, it will still be loaded from default '
|
|
|
|
'configuration.')
|
|
|
|
|
|
|
|
information_opts = opts.add_argument_group("Information")
|
|
|
|
information_opts.add_argument('-p', '--properties', dest='properties', action='store_true', default=None,
|
|
|
|
help='Display properties that can be guessed.')
|
|
|
|
information_opts.add_argument('-V', '--values', dest='values', action='store_true', default=None,
|
|
|
|
help='Display property values that can be guessed.')
|
|
|
|
information_opts.add_argument('--version', dest='version', action='store_true', default=None,
|
|
|
|
help='Display the guessit version.')
|
|
|
|
|
|
|
|
return opts
|
|
|
|
|
|
|
|
|
|
|
|
def parse_options(options=None, api=False):
|
|
|
|
"""
|
|
|
|
Parse given option string
|
|
|
|
|
|
|
|
:param options:
|
|
|
|
:type options:
|
|
|
|
:param api
|
|
|
|
:type api: boolean
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
if isinstance(options, str):
|
|
|
|
args = shlex.split(options)
|
|
|
|
options = vars(argument_parser.parse_args(args))
|
|
|
|
elif options is None:
|
|
|
|
if api:
|
|
|
|
options = {}
|
|
|
|
else:
|
|
|
|
options = vars(argument_parser.parse_args())
|
|
|
|
elif not isinstance(options, dict):
|
|
|
|
options = vars(argument_parser.parse_args(options))
|
|
|
|
return options
|
|
|
|
|
|
|
|
|
|
|
|
argument_parser = build_argument_parser()
|
|
|
|
|
|
|
|
|
|
|
|
class ConfigurationException(Exception):
|
|
|
|
"""
|
|
|
|
Exception related to configuration file.
|
|
|
|
"""
|
|
|
|
pass # pylint:disable=unnecessary-pass
|
|
|
|
|
|
|
|
|
|
|
|
def load_config(options):
|
|
|
|
"""
|
|
|
|
Load options from configuration files, if defined and present.
|
|
|
|
:param options:
|
|
|
|
:type options:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
configurations = []
|
|
|
|
|
|
|
|
if not options.get('no_default_config'):
|
|
|
|
default_options_data = read_text('guessit.config', 'options.json')
|
|
|
|
default_options = json.loads(default_options_data)
|
|
|
|
configurations.append(default_options)
|
|
|
|
|
|
|
|
config_files = []
|
|
|
|
|
|
|
|
if not options.get('no_user_config'):
|
|
|
|
home_directory = os.path.expanduser("~")
|
|
|
|
cwd = os.getcwd()
|
|
|
|
yaml_supported = False
|
|
|
|
try:
|
|
|
|
import yaml # pylint:disable=unused-variable,unused-import,import-outside-toplevel
|
|
|
|
yaml_supported = True
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
config_file_locations = get_options_file_locations(home_directory, cwd, yaml_supported)
|
|
|
|
config_files = [f for f in config_file_locations if os.path.exists(f)]
|
|
|
|
|
|
|
|
custom_config_files = options.get('config')
|
|
|
|
if custom_config_files:
|
|
|
|
config_files = config_files + custom_config_files
|
|
|
|
|
|
|
|
for config_file in config_files:
|
|
|
|
config_file_options = load_config_file(config_file)
|
|
|
|
if config_file_options:
|
|
|
|
configurations.append(config_file_options)
|
|
|
|
|
|
|
|
config = {}
|
|
|
|
if configurations:
|
|
|
|
config = merge_options(*configurations)
|
|
|
|
|
|
|
|
if 'advanced_config' not in config:
|
|
|
|
# Guessit doesn't work without advanced_config, so we use default if no configuration files provides it.
|
|
|
|
default_options_data = read_text('guessit.config', 'options.json')
|
|
|
|
default_options = json.loads(default_options_data)
|
|
|
|
config['advanced_config'] = default_options['advanced_config']
|
|
|
|
|
|
|
|
return config
|
|
|
|
|
|
|
|
|
|
|
|
def merge_options(*options):
|
|
|
|
"""
|
|
|
|
Merge options into a single options dict.
|
|
|
|
:param options:
|
|
|
|
:type options:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
|
|
|
|
merged = {}
|
|
|
|
if options:
|
|
|
|
if options[0]:
|
|
|
|
merged.update(copy.deepcopy(options[0]))
|
|
|
|
|
|
|
|
for options in options[1:]:
|
|
|
|
if options:
|
|
|
|
pristine = options.get('pristine')
|
|
|
|
|
|
|
|
if pristine is True:
|
|
|
|
merged = {}
|
|
|
|
elif pristine:
|
|
|
|
for to_reset in pristine:
|
|
|
|
if to_reset in merged:
|
|
|
|
del merged[to_reset]
|
|
|
|
|
|
|
|
for (option, value) in options.items():
|
|
|
|
merge_option_value(option, value, merged)
|
|
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
def merge_option_value(option, value, merged):
|
|
|
|
"""
|
|
|
|
Merge option value
|
|
|
|
:param option:
|
|
|
|
:param value:
|
|
|
|
:param merged:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
if value is not None and option != 'pristine':
|
|
|
|
if option in merged.keys() and isinstance(merged[option], list):
|
|
|
|
for val in value:
|
|
|
|
if val not in merged[option] and val is not None:
|
|
|
|
merged[option].append(val)
|
|
|
|
elif option in merged.keys() and isinstance(merged[option], dict):
|
|
|
|
merged[option] = merge_options(merged[option], value)
|
|
|
|
elif isinstance(value, list):
|
|
|
|
merged[option] = list(value)
|
|
|
|
else:
|
|
|
|
merged[option] = value
|
|
|
|
|
|
|
|
|
|
|
|
def load_config_file(filepath):
|
|
|
|
"""
|
|
|
|
Load a configuration as an options dict.
|
|
|
|
|
|
|
|
Format of the file is given with filepath extension.
|
|
|
|
:param filepath:
|
|
|
|
:type filepath:
|
|
|
|
:return:
|
|
|
|
:rtype:
|
|
|
|
"""
|
|
|
|
if filepath.endswith('.json'):
|
|
|
|
with open(filepath, encoding='utf-8') as config_file_data:
|
|
|
|
return json.load(config_file_data)
|
|
|
|
if filepath.endswith('.yaml') or filepath.endswith('.yml'):
|
|
|
|
try:
|
|
|
|
import yaml # pylint:disable=import-outside-toplevel
|
|
|
|
with open(filepath, encoding='utf-8') as config_file_data:
|
|
|
|
return yaml.load(config_file_data, yaml.SafeLoader)
|
|
|
|
except ImportError as err: # pragma: no cover
|
|
|
|
raise ConfigurationException('Configuration file extension is not supported. '
|
|
|
|
f'PyYAML should be installed to support "{filepath}" file') from err
|
|
|
|
|
|
|
|
try:
|
|
|
|
# Try to load input as JSON
|
|
|
|
return json.loads(filepath)
|
|
|
|
except: # pylint: disable=bare-except
|
|
|
|
pass
|
|
|
|
|
|
|
|
raise ConfigurationException(f'Configuration file extension is not supported for "{filepath}" file.')
|
|
|
|
|
|
|
|
|
|
|
|
def get_options_file_locations(homedir, cwd, yaml_supported=False):
|
|
|
|
"""
|
|
|
|
Get all possible locations for options file.
|
|
|
|
:param homedir: user home directory
|
|
|
|
:type homedir: basestring
|
|
|
|
:param cwd: current working directory
|
|
|
|
:type homedir: basestring
|
|
|
|
:return:
|
|
|
|
:rtype: list
|
|
|
|
"""
|
|
|
|
locations = []
|
|
|
|
|
|
|
|
configdirs = [(os.path.join(homedir, '.guessit'), 'options'),
|
|
|
|
(os.path.join(homedir, '.config', 'guessit'), 'options'),
|
|
|
|
(cwd, 'guessit.options')]
|
|
|
|
configexts = ['json']
|
|
|
|
|
|
|
|
if yaml_supported:
|
|
|
|
configexts.append('yaml')
|
|
|
|
configexts.append('yml')
|
|
|
|
|
|
|
|
for configdir in configdirs:
|
|
|
|
for configext in configexts:
|
|
|
|
locations.append(os.path.join(configdir[0], configdir[1] + '.' + configext))
|
|
|
|
|
|
|
|
return locations
|