mirror of
https://github.com/clinton-hall/nzbToMedia.git
synced 2025-07-16 02:02:53 -07:00
updated libs to fix guessit and subliminal. Fixes #1080
This commit is contained in:
parent
319d418af8
commit
0625f7f3c0
263 changed files with 28711 additions and 12615 deletions
|
@ -1,197 +1,461 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from __future__ import unicode_literals, print_function
|
||||
import argparse
|
||||
import datetime
|
||||
"""
|
||||
Subliminal uses `click <http://click.pocoo.org>`_ to provide a powerful :abbr:`CLI (command-line interface)`.
|
||||
|
||||
"""
|
||||
from __future__ import division
|
||||
from collections import defaultdict
|
||||
from datetime import timedelta
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import babelfish
|
||||
import xdg.BaseDirectory
|
||||
from subliminal import (__version__, cache_region, MutexLock, provider_manager, Video, Episode, Movie, scan_videos,
|
||||
download_best_subtitles, save_subtitles)
|
||||
try:
|
||||
import colorlog
|
||||
except ImportError:
|
||||
colorlog = None
|
||||
|
||||
from appdirs import AppDirs
|
||||
from babelfish import Error as BabelfishError, Language
|
||||
import click
|
||||
from dogpile.cache.backends.file import AbstractFileLock
|
||||
from dogpile.util.readwrite_lock import ReadWriteMutex
|
||||
from six.moves import configparser
|
||||
|
||||
from subliminal import (AsyncProviderPool, Episode, Movie, Video, __version__, check_video, compute_score, get_scores,
|
||||
provider_manager, refine, refiner_manager, region, save_subtitles, scan_video, scan_videos)
|
||||
from subliminal.core import ARCHIVE_EXTENSIONS, search_external_subtitles
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DEFAULT_CACHE_FILE = os.path.join(xdg.BaseDirectory.save_cache_path('subliminal'), 'cli.dbm')
|
||||
class MutexLock(AbstractFileLock):
|
||||
""":class:`MutexLock` is a thread-based rw lock based on :class:`dogpile.core.ReadWriteMutex`."""
|
||||
def __init__(self, filename):
|
||||
self.mutex = ReadWriteMutex()
|
||||
|
||||
def acquire_read_lock(self, wait):
|
||||
ret = self.mutex.acquire_read_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def acquire_write_lock(self, wait):
|
||||
ret = self.mutex.acquire_write_lock(wait)
|
||||
return wait or ret
|
||||
|
||||
def release_read_lock(self):
|
||||
return self.mutex.release_read_lock()
|
||||
|
||||
def release_write_lock(self):
|
||||
return self.mutex.release_write_lock()
|
||||
|
||||
|
||||
def subliminal():
|
||||
parser = argparse.ArgumentParser(prog='subliminal', description='Subtitles, faster than your thoughts',
|
||||
epilog='Suggestions and bug reports are greatly appreciated: '
|
||||
'https://github.com/Diaoul/subliminal/issues', add_help=False)
|
||||
class Config(object):
|
||||
"""A :class:`~configparser.ConfigParser` wrapper to store configuration.
|
||||
|
||||
# required arguments
|
||||
required_arguments_group = parser.add_argument_group('required arguments')
|
||||
required_arguments_group.add_argument('paths', nargs='+', metavar='PATH', help='path to video file or folder')
|
||||
required_arguments_group.add_argument('-l', '--languages', nargs='+', required=True, metavar='LANGUAGE',
|
||||
help='wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ')
|
||||
Interaction with the configuration is done with the properties.
|
||||
|
||||
# configuration
|
||||
configuration_group = parser.add_argument_group('configuration')
|
||||
configuration_group.add_argument('-s', '--single', action='store_true',
|
||||
help='download without language code in subtitle\'s filename i.e. .srt only')
|
||||
configuration_group.add_argument('-c', '--cache-file', default=DEFAULT_CACHE_FILE,
|
||||
help='cache file (default: %(default)s)')
|
||||
:param str path: path to the configuration file.
|
||||
|
||||
# filtering
|
||||
filtering_group = parser.add_argument_group('filtering')
|
||||
filtering_group.add_argument('-p', '--providers', nargs='+', metavar='PROVIDER',
|
||||
help='providers to use (%s)' % ', '.join(provider_manager.available_providers))
|
||||
filtering_group.add_argument('-m', '--min-score', type=int, default=0,
|
||||
help='minimum score for subtitles (0-%d for episodes, 0-%d for movies)'
|
||||
% (Episode.scores['hash'], Movie.scores['hash']))
|
||||
filtering_group.add_argument('-a', '--age', help='download subtitles for videos newer than AGE e.g. 12h, 1w2d')
|
||||
filtering_group.add_argument('-h', '--hearing-impaired', action='store_true',
|
||||
help='download hearing impaired subtitles')
|
||||
filtering_group.add_argument('-f', '--force', action='store_true',
|
||||
help='force subtitle download for videos with existing subtitles')
|
||||
"""
|
||||
def __init__(self, path):
|
||||
#: Path to the configuration file
|
||||
self.path = path
|
||||
|
||||
# addic7ed
|
||||
addic7ed_group = parser.add_argument_group('addic7ed')
|
||||
addic7ed_group.add_argument('--addic7ed-username', metavar='USERNAME', help='username for addic7ed provider')
|
||||
addic7ed_group.add_argument('--addic7ed-password', metavar='PASSWORD', help='password for addic7ed provider')
|
||||
#: The underlying configuration object
|
||||
self.config = configparser.SafeConfigParser()
|
||||
self.config.add_section('general')
|
||||
self.config.set('general', 'languages', json.dumps(['en']))
|
||||
self.config.set('general', 'providers', json.dumps(sorted([p.name for p in provider_manager])))
|
||||
self.config.set('general', 'refiners', json.dumps(sorted([r.name for r in refiner_manager])))
|
||||
self.config.set('general', 'single', str(0))
|
||||
self.config.set('general', 'embedded_subtitles', str(1))
|
||||
self.config.set('general', 'age', str(int(timedelta(weeks=2).total_seconds())))
|
||||
self.config.set('general', 'hearing_impaired', str(1))
|
||||
self.config.set('general', 'min_score', str(0))
|
||||
|
||||
# output
|
||||
output_group = parser.add_argument_group('output')
|
||||
output_group.add_argument('-d', '--directory',
|
||||
help='save subtitles in the given directory rather than next to the video')
|
||||
output_group.add_argument('-e', '--encoding', default=None,
|
||||
help='encoding to convert the subtitle to (default: no conversion)')
|
||||
output_exclusive_group = output_group.add_mutually_exclusive_group()
|
||||
output_exclusive_group.add_argument('-q', '--quiet', action='store_true', help='disable output')
|
||||
output_exclusive_group.add_argument('-v', '--verbose', action='store_true', help='verbose output')
|
||||
output_group.add_argument('--log-file', help='log into a file instead of stdout')
|
||||
output_group.add_argument('--color', action='store_true', help='add color to console output (requires colorlog)')
|
||||
def read(self):
|
||||
"""Read the configuration from :attr:`path`"""
|
||||
self.config.read(self.path)
|
||||
|
||||
# troubleshooting
|
||||
troubleshooting_group = parser.add_argument_group('troubleshooting')
|
||||
troubleshooting_group.add_argument('--debug', action='store_true', help='debug output')
|
||||
troubleshooting_group.add_argument('--version', action='version', version=__version__)
|
||||
troubleshooting_group.add_argument('--help', action='help', help='show this help message and exit')
|
||||
def write(self):
|
||||
"""Write the configuration to :attr:`path`"""
|
||||
with open(self.path, 'w') as f:
|
||||
self.config.write(f)
|
||||
|
||||
# parse args
|
||||
args = parser.parse_args()
|
||||
@property
|
||||
def languages(self):
|
||||
return {Language.fromietf(l) for l in json.loads(self.config.get('general', 'languages'))}
|
||||
|
||||
# parse paths
|
||||
try:
|
||||
args.paths = [os.path.abspath(os.path.expanduser(p.decode('utf-8') if isinstance(p, bytes) else p))
|
||||
for p in args.paths]
|
||||
except UnicodeDecodeError:
|
||||
parser.error('argument paths: encodings is not utf-8: %r' % args.paths)
|
||||
@languages.setter
|
||||
def languages(self, value):
|
||||
self.config.set('general', 'languages', json.dumps(sorted([str(l) for l in value])))
|
||||
|
||||
# parse languages
|
||||
try:
|
||||
args.languages = {babelfish.Language.fromietf(l) for l in args.languages}
|
||||
except babelfish.Error:
|
||||
parser.error('argument -l/--languages: codes are not IETF: %r' % args.languages)
|
||||
@property
|
||||
def providers(self):
|
||||
return json.loads(self.config.get('general', 'providers'))
|
||||
|
||||
# parse age
|
||||
if args.age is not None:
|
||||
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', args.age)
|
||||
@providers.setter
|
||||
def providers(self, value):
|
||||
self.config.set('general', 'providers', json.dumps(sorted([p.lower() for p in value])))
|
||||
|
||||
@property
|
||||
def refiners(self):
|
||||
return json.loads(self.config.get('general', 'refiners'))
|
||||
|
||||
@refiners.setter
|
||||
def refiners(self, value):
|
||||
self.config.set('general', 'refiners', json.dumps([r.lower() for r in value]))
|
||||
|
||||
@property
|
||||
def single(self):
|
||||
return self.config.getboolean('general', 'single')
|
||||
|
||||
@single.setter
|
||||
def single(self, value):
|
||||
self.config.set('general', 'single', str(int(value)))
|
||||
|
||||
@property
|
||||
def embedded_subtitles(self):
|
||||
return self.config.getboolean('general', 'embedded_subtitles')
|
||||
|
||||
@embedded_subtitles.setter
|
||||
def embedded_subtitles(self, value):
|
||||
self.config.set('general', 'embedded_subtitles', str(int(value)))
|
||||
|
||||
@property
|
||||
def age(self):
|
||||
return timedelta(seconds=self.config.getint('general', 'age'))
|
||||
|
||||
@age.setter
|
||||
def age(self, value):
|
||||
self.config.set('general', 'age', str(int(value.total_seconds())))
|
||||
|
||||
@property
|
||||
def hearing_impaired(self):
|
||||
return self.config.getboolean('general', 'hearing_impaired')
|
||||
|
||||
@hearing_impaired.setter
|
||||
def hearing_impaired(self, value):
|
||||
self.config.set('general', 'hearing_impaired', str(int(value)))
|
||||
|
||||
@property
|
||||
def min_score(self):
|
||||
return self.config.getfloat('general', 'min_score')
|
||||
|
||||
@min_score.setter
|
||||
def min_score(self, value):
|
||||
self.config.set('general', 'min_score', str(value))
|
||||
|
||||
@property
|
||||
def provider_configs(self):
|
||||
rv = {}
|
||||
for provider in provider_manager:
|
||||
if self.config.has_section(provider.name):
|
||||
rv[provider.name] = {k: v for k, v in self.config.items(provider.name)}
|
||||
return rv
|
||||
|
||||
@provider_configs.setter
|
||||
def provider_configs(self, value):
|
||||
# loop over provider configurations
|
||||
for provider, config in value.items():
|
||||
# create the corresponding section if necessary
|
||||
if not self.config.has_section(provider):
|
||||
self.config.add_section(provider)
|
||||
|
||||
# add config options
|
||||
for k, v in config.items():
|
||||
self.config.set(provider, k, v)
|
||||
|
||||
|
||||
class LanguageParamType(click.ParamType):
|
||||
""":class:`~click.ParamType` for languages that returns a :class:`~babelfish.language.Language`"""
|
||||
name = 'language'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
try:
|
||||
return Language.fromietf(value)
|
||||
except BabelfishError:
|
||||
self.fail('%s is not a valid language' % value)
|
||||
|
||||
LANGUAGE = LanguageParamType()
|
||||
|
||||
|
||||
class AgeParamType(click.ParamType):
|
||||
""":class:`~click.ParamType` for age strings that returns a :class:`~datetime.timedelta`
|
||||
|
||||
An age string is in the form `number + identifier` with possible identifiers:
|
||||
|
||||
* ``w`` for weeks
|
||||
* ``d`` for days
|
||||
* ``h`` for hours
|
||||
|
||||
The form can be specified multiple times but only with that idenfier ordering. For example:
|
||||
|
||||
* ``1w2d4h`` for 1 week, 2 days and 4 hours
|
||||
* ``2w`` for 2 weeks
|
||||
* ``3w6h`` for 3 weeks and 6 hours
|
||||
|
||||
"""
|
||||
name = 'age'
|
||||
|
||||
def convert(self, value, param, ctx):
|
||||
match = re.match(r'^(?:(?P<weeks>\d+?)w)?(?:(?P<days>\d+?)d)?(?:(?P<hours>\d+?)h)?$', value)
|
||||
if not match:
|
||||
parser.error('argument -a/--age: invalid age: %r' % args.age)
|
||||
args.age = datetime.timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
|
||||
self.fail('%s is not a valid age' % value)
|
||||
|
||||
# parse cache-file
|
||||
args.cache_file = os.path.abspath(os.path.expanduser(args.cache_file))
|
||||
if not os.path.exists(os.path.split(args.cache_file)[0]):
|
||||
parser.error('argument -c/--cache-file: directory %r for cache file does not exist'
|
||||
% os.path.split(args.cache_file)[0])
|
||||
return timedelta(**{k: int(v) for k, v in match.groupdict(0).items()})
|
||||
|
||||
# parse provider configs
|
||||
provider_configs = {}
|
||||
if (args.addic7ed_username is not None and args.addic7ed_password is None
|
||||
or args.addic7ed_username is None and args.addic7ed_password is not None):
|
||||
parser.error('argument --addic7ed-username/--addic7ed-password: both arguments are required or none')
|
||||
if args.addic7ed_username is not None and args.addic7ed_password is not None:
|
||||
provider_configs['addic7ed'] = {'username': args.addic7ed_username, 'password': args.addic7ed_password}
|
||||
AGE = AgeParamType()
|
||||
|
||||
# parse color
|
||||
if args.color and colorlog is None:
|
||||
parser.error('argument --color: colorlog required')
|
||||
PROVIDER = click.Choice(sorted(provider_manager.names()))
|
||||
|
||||
# setup output
|
||||
if args.log_file is None:
|
||||
handler = logging.StreamHandler()
|
||||
else:
|
||||
handler = logging.FileHandler(args.log_file, encoding='utf-8')
|
||||
if args.debug:
|
||||
if args.color:
|
||||
if args.log_file is None:
|
||||
log_format = '%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s'
|
||||
else:
|
||||
log_format = '%(purple)s%(asctime)s%(reset)s %(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s-%(funcName)s:%(lineno)d%(reset)s] %(message)s'
|
||||
handler.setFormatter(colorlog.ColoredFormatter(log_format,
|
||||
log_colors=dict(colorlog.default_log_colors.items() + [('DEBUG', 'cyan')])))
|
||||
else:
|
||||
if args.log_file is None:
|
||||
log_format = '%(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'
|
||||
else:
|
||||
log_format = '%(asctime)s %(levelname)-8s [%(name)s-%(funcName)s:%(lineno)d] %(message)s'
|
||||
handler.setFormatter(logging.Formatter(log_format))
|
||||
logging.getLogger().addHandler(handler)
|
||||
logging.getLogger().setLevel(logging.DEBUG)
|
||||
elif args.verbose:
|
||||
if args.color:
|
||||
if args.log_file is None:
|
||||
log_format = '%(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'
|
||||
else:
|
||||
log_format = '%(purple)s%(asctime)s%(reset)s %(log_color)s%(levelname)-8s%(reset)s [%(blue)s%(name)s%(reset)s] %(message)s'
|
||||
handler.setFormatter(colorlog.ColoredFormatter(log_format))
|
||||
else:
|
||||
log_format = '%(levelname)-8s [%(name)s] %(message)s'
|
||||
if args.log_file is not None:
|
||||
log_format = '%(asctime)s ' + log_format
|
||||
handler.setFormatter(logging.Formatter(log_format))
|
||||
logging.getLogger('subliminal').addHandler(handler)
|
||||
logging.getLogger('subliminal').setLevel(logging.INFO)
|
||||
elif not args.quiet:
|
||||
if args.color:
|
||||
if args.log_file is None:
|
||||
log_format = '[%(log_color)s%(levelname)s%(reset)s] %(message)s'
|
||||
else:
|
||||
log_format = '%(purple)s%(asctime)s%(reset)s [%(log_color)s%(levelname)s%(reset)s] %(message)s'
|
||||
handler.setFormatter(colorlog.ColoredFormatter(log_format))
|
||||
else:
|
||||
if args.log_file is None:
|
||||
log_format = '%(levelname)s: %(message)s'
|
||||
else:
|
||||
log_format = '%(asctime)s %(levelname)s: %(message)s'
|
||||
handler.setFormatter(logging.Formatter(log_format))
|
||||
logging.getLogger('subliminal.api').addHandler(handler)
|
||||
logging.getLogger('subliminal.api').setLevel(logging.INFO)
|
||||
REFINER = click.Choice(sorted(refiner_manager.names()))
|
||||
|
||||
dirs = AppDirs('subliminal')
|
||||
cache_file = 'subliminal.dbm'
|
||||
config_file = 'config.ini'
|
||||
|
||||
|
||||
@click.group(context_settings={'max_content_width': 100}, epilog='Suggestions and bug reports are greatly appreciated: '
|
||||
'https://github.com/Diaoul/subliminal/')
|
||||
@click.option('--addic7ed', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='Addic7ed configuration.')
|
||||
@click.option('--legendastv', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='LegendasTV configuration.')
|
||||
@click.option('--opensubtitles', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD',
|
||||
help='OpenSubtitles configuration.')
|
||||
@click.option('--subscenter', type=click.STRING, nargs=2, metavar='USERNAME PASSWORD', help='SubsCenter configuration.')
|
||||
@click.option('--cache-dir', type=click.Path(writable=True, file_okay=False), default=dirs.user_cache_dir,
|
||||
show_default=True, expose_value=True, help='Path to the cache directory.')
|
||||
@click.option('--debug', is_flag=True, help='Print useful information for debugging subliminal and for reporting bugs.')
|
||||
@click.version_option(__version__)
|
||||
@click.pass_context
|
||||
def subliminal(ctx, addic7ed, legendastv, opensubtitles, subscenter, cache_dir, debug):
|
||||
"""Subtitles, faster than your thoughts."""
|
||||
# create cache directory
|
||||
try:
|
||||
os.makedirs(cache_dir)
|
||||
except OSError:
|
||||
if not os.path.isdir(cache_dir):
|
||||
raise
|
||||
|
||||
# configure cache
|
||||
cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30), # @UndefinedVariable
|
||||
arguments={'filename': args.cache_file, 'lock_factory': MutexLock})
|
||||
region.configure('dogpile.cache.dbm', expiration_time=timedelta(days=30),
|
||||
arguments={'filename': os.path.join(cache_dir, cache_file), 'lock_factory': MutexLock})
|
||||
|
||||
# configure logging
|
||||
if debug:
|
||||
handler = logging.StreamHandler()
|
||||
handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
|
||||
logging.getLogger('subliminal').addHandler(handler)
|
||||
logging.getLogger('subliminal').setLevel(logging.DEBUG)
|
||||
|
||||
# provider configs
|
||||
ctx.obj = {'provider_configs': {}}
|
||||
if addic7ed:
|
||||
ctx.obj['provider_configs']['addic7ed'] = {'username': addic7ed[0], 'password': addic7ed[1]}
|
||||
if legendastv:
|
||||
ctx.obj['provider_configs']['legendastv'] = {'username': legendastv[0], 'password': legendastv[1]}
|
||||
if opensubtitles:
|
||||
ctx.obj['provider_configs']['opensubtitles'] = {'username': opensubtitles[0], 'password': opensubtitles[1]}
|
||||
if subscenter:
|
||||
ctx.obj['provider_configs']['subscenter'] = {'username': subscenter[0], 'password': subscenter[1]}
|
||||
|
||||
|
||||
@subliminal.command()
|
||||
@click.option('--clear-subliminal', is_flag=True, help='Clear subliminal\'s cache. Use this ONLY if your cache is '
|
||||
'corrupted or if you experience issues.')
|
||||
@click.pass_context
|
||||
def cache(ctx, clear_subliminal):
|
||||
"""Cache management."""
|
||||
if clear_subliminal:
|
||||
for file in glob.glob(os.path.join(ctx.parent.params['cache_dir'], cache_file) + '*'):
|
||||
os.remove(file)
|
||||
click.echo('Subliminal\'s cache cleared.')
|
||||
else:
|
||||
click.echo('Nothing done.')
|
||||
|
||||
|
||||
@subliminal.command()
|
||||
@click.option('-l', '--language', type=LANGUAGE, required=True, multiple=True, help='Language as IETF code, '
|
||||
'e.g. en, pt-BR (can be used multiple times).')
|
||||
@click.option('-p', '--provider', type=PROVIDER, multiple=True, help='Provider to use (can be used multiple times).')
|
||||
@click.option('-r', '--refiner', type=REFINER, multiple=True, help='Refiner to use (can be used multiple times).')
|
||||
@click.option('-a', '--age', type=AGE, help='Filter videos newer than AGE, e.g. 12h, 1w2d.')
|
||||
@click.option('-d', '--directory', type=click.STRING, metavar='DIR', help='Directory where to save subtitles, '
|
||||
'default is next to the video file.')
|
||||
@click.option('-e', '--encoding', type=click.STRING, metavar='ENC', help='Subtitle file encoding, default is to '
|
||||
'preserve original encoding.')
|
||||
@click.option('-s', '--single', is_flag=True, default=False, help='Save subtitle without language code in the file '
|
||||
'name, i.e. use .srt extension. Do not use this unless your media player requires it.')
|
||||
@click.option('-f', '--force', is_flag=True, default=False, help='Force download even if a subtitle already exist.')
|
||||
@click.option('-hi', '--hearing-impaired', is_flag=True, default=False, help='Prefer hearing impaired subtitles.')
|
||||
@click.option('-m', '--min-score', type=click.IntRange(0, 100), default=0, help='Minimum score for a subtitle '
|
||||
'to be downloaded (0 to 100).')
|
||||
@click.option('-w', '--max-workers', type=click.IntRange(1, 50), default=None, help='Maximum number of threads to use.')
|
||||
@click.option('-z/-Z', '--archives/--no-archives', default=True, show_default=True, help='Scan archives for videos '
|
||||
'(supported extensions: %s).' % ', '.join(ARCHIVE_EXTENSIONS))
|
||||
@click.option('-v', '--verbose', count=True, help='Increase verbosity.')
|
||||
@click.argument('path', type=click.Path(), required=True, nargs=-1)
|
||||
@click.pass_obj
|
||||
def download(obj, provider, refiner, language, age, directory, encoding, single, force, hearing_impaired, min_score,
|
||||
max_workers, archives, verbose, path):
|
||||
"""Download best subtitles.
|
||||
|
||||
PATH can be an directory containing videos, a video file path or a video file name. It can be used multiple times.
|
||||
|
||||
If an existing subtitle is detected (external or embedded) in the correct language, the download is skipped for
|
||||
the associated video.
|
||||
|
||||
"""
|
||||
# process parameters
|
||||
language = set(language)
|
||||
|
||||
# scan videos
|
||||
videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force,
|
||||
embedded_subtitles=not args.force, age=args.age)
|
||||
videos = []
|
||||
ignored_videos = []
|
||||
errored_paths = []
|
||||
with click.progressbar(path, label='Collecting videos', item_show_func=lambda p: p or '') as bar:
|
||||
for p in bar:
|
||||
logger.debug('Collecting path %s', p)
|
||||
|
||||
# guess videos
|
||||
videos.extend([Video.fromname(p) for p in args.paths if not os.path.exists(p)])
|
||||
# non-existing
|
||||
if not os.path.exists(p):
|
||||
try:
|
||||
video = Video.fromname(p)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting non-existing path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
continue
|
||||
|
||||
# directories
|
||||
if os.path.isdir(p):
|
||||
try:
|
||||
scanned_videos = scan_videos(p, age=age, archives=archives)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting directory path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
for video in scanned_videos:
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name,
|
||||
directory=directory).values())
|
||||
if check_video(video, languages=language, age=age, undefined=single):
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
else:
|
||||
ignored_videos.append(video)
|
||||
continue
|
||||
|
||||
# other inputs
|
||||
try:
|
||||
video = scan_video(p)
|
||||
except:
|
||||
logger.exception('Unexpected error while collecting path %s', p)
|
||||
errored_paths.append(p)
|
||||
continue
|
||||
if not force:
|
||||
video.subtitle_languages |= set(search_external_subtitles(video.name, directory=directory).values())
|
||||
if check_video(video, languages=language, age=age, undefined=single):
|
||||
refine(video, episode_refiners=refiner, movie_refiners=refiner, embedded_subtitles=not force)
|
||||
videos.append(video)
|
||||
else:
|
||||
ignored_videos.append(video)
|
||||
|
||||
# output errored paths
|
||||
if verbose > 0:
|
||||
for p in errored_paths:
|
||||
click.secho('%s errored' % p, fg='red')
|
||||
|
||||
# output ignored videos
|
||||
if verbose > 1:
|
||||
for video in ignored_videos:
|
||||
click.secho('%s ignored - subtitles: %s / age: %d day%s' % (
|
||||
os.path.split(video.name)[1],
|
||||
', '.join(str(s) for s in video.subtitle_languages) or 'none',
|
||||
video.age.days,
|
||||
's' if video.age.days > 1 else ''
|
||||
), fg='yellow')
|
||||
|
||||
# report collected videos
|
||||
click.echo('%s video%s collected / %s video%s ignored / %s error%s' % (
|
||||
click.style(str(len(videos)), bold=True, fg='green' if videos else None),
|
||||
's' if len(videos) > 1 else '',
|
||||
click.style(str(len(ignored_videos)), bold=True, fg='yellow' if ignored_videos else None),
|
||||
's' if len(ignored_videos) > 1 else '',
|
||||
click.style(str(len(errored_paths)), bold=True, fg='red' if errored_paths else None),
|
||||
's' if len(errored_paths) > 1 else '',
|
||||
))
|
||||
|
||||
# exit if no video collected
|
||||
if not videos:
|
||||
return
|
||||
|
||||
# download best subtitles
|
||||
subtitles = download_best_subtitles(videos, args.languages, providers=args.providers,
|
||||
provider_configs=provider_configs, min_score=args.min_score,
|
||||
hearing_impaired=args.hearing_impaired, single=args.single)
|
||||
downloaded_subtitles = defaultdict(list)
|
||||
with AsyncProviderPool(max_workers=max_workers, providers=provider, provider_configs=obj['provider_configs']) as p:
|
||||
with click.progressbar(videos, label='Downloading subtitles',
|
||||
item_show_func=lambda v: os.path.split(v.name)[1] if v is not None else '') as bar:
|
||||
for v in bar:
|
||||
scores = get_scores(v)
|
||||
subtitles = p.download_best_subtitles(p.list_subtitles(v, language - v.subtitle_languages),
|
||||
v, language, min_score=scores['hash'] * min_score / 100,
|
||||
hearing_impaired=hearing_impaired, only_one=single)
|
||||
downloaded_subtitles[v] = subtitles
|
||||
|
||||
if p.discarded_providers:
|
||||
click.secho('Some providers have been discarded due to unexpected errors: %s' %
|
||||
', '.join(p.discarded_providers), fg='yellow')
|
||||
|
||||
# save subtitles
|
||||
save_subtitles(subtitles, single=args.single, directory=args.directory, encoding=args.encoding)
|
||||
total_subtitles = 0
|
||||
for v, subtitles in downloaded_subtitles.items():
|
||||
saved_subtitles = save_subtitles(v, subtitles, single=single, directory=directory, encoding=encoding)
|
||||
total_subtitles += len(saved_subtitles)
|
||||
|
||||
# result output
|
||||
if not subtitles:
|
||||
if not args.quiet:
|
||||
print('No subtitles downloaded', file=sys.stderr)
|
||||
exit(1)
|
||||
if not args.quiet:
|
||||
subtitles_count = sum([len(s) for s in subtitles.values()])
|
||||
if subtitles_count == 1:
|
||||
print('%d subtitle downloaded' % subtitles_count)
|
||||
else:
|
||||
print('%d subtitles downloaded' % subtitles_count)
|
||||
if verbose > 0:
|
||||
click.echo('%s subtitle%s downloaded for %s' % (click.style(str(len(saved_subtitles)), bold=True),
|
||||
's' if len(saved_subtitles) > 1 else '',
|
||||
os.path.split(v.name)[1]))
|
||||
|
||||
if verbose > 1:
|
||||
for s in saved_subtitles:
|
||||
matches = s.get_matches(v)
|
||||
score = compute_score(s, v)
|
||||
|
||||
# score color
|
||||
score_color = None
|
||||
scores = get_scores(v)
|
||||
if isinstance(v, Movie):
|
||||
if score < scores['title']:
|
||||
score_color = 'red'
|
||||
elif score < scores['title'] + scores['year'] + scores['release_group']:
|
||||
score_color = 'yellow'
|
||||
else:
|
||||
score_color = 'green'
|
||||
elif isinstance(v, Episode):
|
||||
if score < scores['series'] + scores['season'] + scores['episode']:
|
||||
score_color = 'red'
|
||||
elif score < scores['series'] + scores['season'] + scores['episode'] + scores['release_group']:
|
||||
score_color = 'yellow'
|
||||
else:
|
||||
score_color = 'green'
|
||||
|
||||
# scale score from 0 to 100 taking out preferences
|
||||
scaled_score = score
|
||||
if s.hearing_impaired == hearing_impaired:
|
||||
scaled_score -= scores['hearing_impaired']
|
||||
scaled_score *= 100 / scores['hash']
|
||||
|
||||
# echo some nice colored output
|
||||
click.echo(' - [{score}] {language} subtitle from {provider_name} (match on {matches})'.format(
|
||||
score=click.style('{:5.1f}'.format(scaled_score), fg=score_color, bold=score >= scores['hash']),
|
||||
language=s.language.name if s.language.country is None else '%s (%s)' % (s.language.name,
|
||||
s.language.country.name),
|
||||
provider_name=s.provider_name,
|
||||
matches=', '.join(sorted(matches, key=scores.get, reverse=True))
|
||||
))
|
||||
|
||||
if verbose == 0:
|
||||
click.echo('Downloaded %s subtitle%s' % (click.style(str(total_subtitles), bold=True),
|
||||
's' if total_subtitles > 1 else ''))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue