Example #1
0
def subliminal():
    parser = subliminal_parser()
    args = parser.parse_args()

    # parse paths
    try:
        args.paths = [p.decode('utf-8') for p in args.paths]
    except UnicodeDecodeError:
        parser.error('argument paths: encodings is not utf-8: %r' % args.paths)

    # parse languages
    try:
        args.languages = {babelfish.Language.fromalpha2(l) for l in args.languages}
    except babelfish.Error:
        parser.error('argument -l/--languages: codes are not ISO-639-1: %r' % args.languages)

    # 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)
        if not match:
            parser.error('argument -a/--age: invalid age: %r' % args.age)
        args.age = datetime.timedelta(**match.groupdict())

    # setup verbosity
    if args.verbose:
        logging.basicConfig(level=logging.DEBUG)
    elif not args.quiet:
        logging.basicConfig(level=logging.WARN)

    # configure cache
    cache_region.configure('dogpile.cache.dbm', arguments={'filename': os.path.expanduser(args.cache_file)})

    # scan videos
    videos = scan_videos([p for p in args.paths if os.path.exists(p)], subtitles=not args.force, age=args.age)

    # guess videos
    videos.extend([Video.fromguess(os.path.split(p)[1], guessit.guess_file_info(p, 'autodetect')) for p in args.paths
                   if not os.path.exists(p)])

    # download best subtitles
    subtitles = download_best_subtitles(videos, args.languages, providers=args.providers, provider_configs=None,
                                        single=args.single, min_score=args.min_score,
                                        hearing_impaired=args.hearing_impaired)

    # output result
    if not subtitles:
        if not args.quiet:
            sys.stderr.write('No subtitles downloaded\n')
        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)
Example #2
0
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)

    # 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 ')

    # 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)')

    # 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')

    # 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')

    # 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='utf-8', help='subtitles encoding (default: %(default)s)')
    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)')

    # 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')

    # parse args
    args = parser.parse_args()

    # 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)

    # 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)

    # 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)
        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()})

    # 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])

    # 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}

    # parse color
    if args.color and colorlog is None:
        parser.error('argument --color: colorlog required')

    # 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)

    # configure cache
    cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30),  # @UndefinedVariable
                           arguments={'filename': args.cache_file, 'lock_factory': MutexLock})

    # 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)

    # guess videos
    videos.extend([Video.fromname(p) for p in args.paths if not os.path.exists(p)])

    # 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)

    # save subtitles
    save_subtitles(subtitles, single=args.single, directory=args.directory, encoding=args.encoding)

    # 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)
    info = {}
    if request.method == 'GET':
        for key, value in request.args.lists():
            info[key] = value[0]
    else:
        for key, value in request.form.lists():
            info[key] = value[0]
    if request.files:
        info['post_file'] = {}
        for key, value in request.files.lists():
            info['post_file'][key] = value[0]
    return info

@app.route('/search', methods=['GET'])
def search():
    params = get_params(request)
    infos = search_for_subtitle(params.get('file_name'), params.get('languages'))
    return flask.Response(json.dumps(infos),  mimetype='application/json')

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    if not cache_region.is_configured:
        cache_region.configure('dogpile.cache.dbm', expiration_time=datetime.timedelta(days=30),  # @UndefinedVariable
                           arguments={'filename':BASE_DIR, 'lock_factory': MutexLock})
    app.run()


Example #4
0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal

cache_region.configure('dogpile.cache.memory',
                       expiration_time=60 * 30)  # @UndefinedVariable
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])

if __name__ == '__main__':
    TextTestRunner().run(suite)
Example #5
0
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)

    # 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 ')

    # 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)')

    # 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')

    # 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')

    # 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)')

    # 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')

    # parse args
    args = parser.parse_args()

    # 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)

    # 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)

    # 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)
        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()})

    # 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])

    # 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
        }

    # parse color
    if args.color and colorlog is None:
        parser.error('argument --color: colorlog required')

    # 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)

    # configure cache
    cache_region.configure(
        'dogpile.cache.dbm',
        expiration_time=datetime.timedelta(days=30),  # @UndefinedVariable
        arguments={
            'filename': args.cache_file,
            'lock_factory': MutexLock
        })

    # 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)

    # guess videos
    videos.extend(
        [Video.fromname(p) for p in args.paths if not os.path.exists(p)])

    # 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)

    # save subtitles
    save_subtitles(subtitles,
                   single=args.single,
                   directory=args.directory,
                   encoding=args.encoding)

    # 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)
Example #6
0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal


cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)  # @UndefinedVariable
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])


if __name__ == '__main__':
    TextTestRunner().run(suite)
Example #7
0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal


cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])


if __name__ == '__main__':
    TextTestRunner().run(suite)
Example #8
0
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from unittest import TextTestRunner, TestSuite
from subliminal import cache_region
from . import test_providers, test_subliminal

cache_region.configure('dogpile.cache.memory', expiration_time=60 * 30)
suite = TestSuite([test_providers.suite(), test_subliminal.suite()])

if __name__ == '__main__':
    TextTestRunner().run(suite)
Example #9
0
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,
    )

    # 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",
        action="append",
        required=True,
        metavar="LANGUAGE",
        help="wanted languages as IETF codes e.g. fr, pt-BR, sr-Cyrl ",
    )

    # 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)"
    )
    configuration_group.add_argument("--all", action="store_true", help="download all subtitles")

    # filtering
    filtering_group = parser.add_argument_group("filtering")
    filtering_group.add_argument(
        "-p",
        "--providers",
        action="append",
        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"
    )

    # 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")

    # 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)")

    # 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")

    # parse args
    args = parser.parse_args()

    # 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)

    # 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)

    # 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)
        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()})

    # 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]
        )

    # 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}

    # parse color
    if args.color and colorlog is None:
        parser.error("argument --color: colorlog required")

    # 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)

    # configure cache
    cache_region.configure(
        "dogpile.cache.dbm",
        expiration_time=datetime.timedelta(days=30),  # @UndefinedVariable
        arguments={"filename": args.cache_file, "lock_factory": MutexLock},
    )

    # 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,
    )

    # guess videos
    videos.extend([Video.fromname(p) for p in args.paths if not os.path.exists(p)])

    if args.all:
        subtitles = collections.defaultdict(list)
        with ProviderPool(args.providers, provider_configs) as pp:
            for video in videos:
                for subtitle in pp.list_subtitles(video, args.languages):
                    if pp.download_subtitle(subtitle):
                        subtitles[video].append(subtitle)
    else:
        # 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,
        )

    # save subtitles
    save_subtitles(
        subtitles, single=args.single, directory=args.directory, encoding=args.encoding, save_multiple=args.all
    )

    # 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)