#! /usr/bin/env nix-shell #! nix-shell -i python3 -p python3 # subdl - command-line tool to download subtitles from opensubtitles.org. # # Uses code from subdownloader (a GUI app). __doc__ = '''\ Syntax: subdl [options] moviefile.avi ... Subdl is a command-line tool for downloading subtitles from opensubtitles.org. By default, it will search for English subtitles, display the results, download the highest-rated result in the requested language and save it to the appropriate filename. Options: --help This text --version Print version and exit --lang=LANGUAGES Comma-separated list of languages in 3-letter code, e.g. 'eng,spa,fre', or 'all' for all. Default is 'eng'. --list-languages List available languages and exit. --download=ID Download a particular subtitle by numeric ID. --download=first Download the first search result [default]. --download=all Download all search results. --download=best-rating Download the result with best rating. --download=most-downloaded Download the most downloaded result. --download=query Query which search result to download. --download=none, -n Display search results and exit. --output=OUTPUT Output to specified output filename. Can include the following format specifiers: %I subtitle id %m movie file base %M movie file extension %s subtitle file base %S subtitle file extension %l language (English) %L language (2-letter ISO639) Default is "%m.%S"; if multiple languages are searched, then the default is "%m.%L.%S"; if --download=all, then the default is "%m.%L.%I.%S". --existing=abort Abort if output filename already exists [default]. --existing=bypass Exit gracefully if output filename already exists. --existing=overwrite Overwrite if output filename already exists. --existing=query Query whether to overwrite. --imdb-id=id Query by IMDB id. Hash is tried first unless --force-imdb is used. IMDB URLs are also accepted. --force-imdb Force IMDB search. --imdb-id must be specified. --force-filename Force search using filename. --filter Filter blacklisted texts from subtitle. --interactive, -i Equivalent to --download=query --existing=query. ''' NAME = 'subdl' VERSION = '1.0.3' VERSION_INFO = '''\ This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. http://code.google.com/p/subdl/''' import os, sys import struct import xmlrpc.client import io, gzip, base64 import getopt import re osdb_server = "https://api.opensubtitles.org/xml-rpc" xmlrpc_server = xmlrpc.client.ServerProxy(osdb_server) login = xmlrpc_server.LogIn("", "", "en", NAME+" "+VERSION) osdb_token = login["token"] BLACKLIST = [ 'opensubtitles', 'addic7ed', 'joycasino', 'bitninja\.io', 'Please rate this subtitle at www\.osdb\.link', 'allsubs', 'firebit\.org', 'humanguardians\.com', 'subtitles by', 'recast\.ai', 'by mstoll', 'subs corrected', 'by tronar', 'titlovi', '^_$', '^- _$', ] class Options: pass options = Options() options.lang = 'eng' options.download = 'first' options.output = None options.existing = 'abort' options.imdb_id = None options.force_imdb = False options.force_filename = False options.filter = False class SubtitleSearchResult: def __init__(self, dict): self.__dict__ = dict def file_ext(filename): return os.path.splitext(filename)[1][1:] def file_base(filename): return os.path.splitext(filename)[0] def gunzipstr(zs): with gzip.open(io.BytesIO(zs)) as gz: return gz.read() def writefile(filename, str): try: with open(filename, 'wb') as f: f.write(str) except Exception as e: raise SystemExit("Error writing to %s: %s" % (filename, e)) def query_num(s, low, high): while True: try: n = input("%s [%d..%d] " % (s, low, high)) except KeyboardInterrupt: raise SystemExit("Aborted") try: n = int(n) if low <= n <= high: return n except: pass def query_yn(s): while True: try: r = input("%s [y/n] " % s).lower() except KeyboardInterrupt: raise SystemExit("Aborted") if r.startswith('y'): return True elif r.startswith('n'): return False def filtersub(s): s = s.strip() line_sep = b'\r\n' if re.search(b'\r\n', s) else b'\n' subs = re.split(b'(?:\r?\n){2,}', s) subs = [re.split(b'\r?\n', sub, 2) for sub in subs] filter_pattern = re.compile('|'.join(BLACKLIST).encode(), re.M | re.I) for i in range(len(subs) - 1, -1, -1): if len(subs[i]) < 3: del subs[i] continue text = subs[i][2] if filter_pattern.search(text): print("Removed", i + 1, ":", text) del subs[i] for i in range(len(subs)): subs[i][0] = str(i + 1).encode() subs = map(line_sep.join, subs) return (line_sep * 2).join(subs) def movie_hash(name): longlongformat = ' 0 except: return False def ListLanguages(): languages = xmlrpc_server.GetSubLanguages('')['data'] for language in languages: print(language['SubLanguageID'], language['ISO639'], language['LanguageName']) raise SystemExit def default_output_fmt(): if options.download == 'all': return "{m}.{L}.{I}.{S}" elif options.lang == 'all' or ',' in options.lang: return "{m}.{L}.{S}" else: return "{m}.{S}" def parseargs(args): try: opts, arguments = getopt.getopt(args, 'h?in', [ 'existing=', 'lang=', 'search-only=', 'download=', 'output=', 'interactive', 'list-languages', 'imdb-id=', 'force-imdb', 'force-filename', 'filter', 'help', 'version', 'versionx']) except getopt.GetoptError as e: raise SystemExit("%s: %s (see --help)" % (sys.argv[0], e)) for option, value in opts: if option == '--help' or option == '-h' or option == '-?': help() elif option == '--versionx': print(VERSION) raise SystemExit elif option == '--version': print("%s %s" % (NAME, VERSION)) raise SystemExit elif option == '--existing': if value in ['abort', 'overwrite', 'bypass', 'query']: pass else: raise SystemExit("Argument to --existing must be one of: abort, overwrite, bypass, query") options.existing = value elif option == '--lang': options.lang = value elif option == '--download': if value in ['all', 'first', 'query', 'none', 'best-rating', 'most-downloaded'] or isnumber(value): pass else: raise SystemExit("Argument to --download must be numeric subtitle id or one: all, first, query, none") options.download = value elif option == '-n': options.download = 'none' elif option == '--output': options.output = value elif option == '--imdb-id': options.imdb_id = value elif option == '--force-imdb': options.force_imdb = True elif option == '--force-filename': options.force_filename = True elif option == '--filter': options.filter = True elif option == '--interactive' or option == '-i': options.download = 'query' options.existing = 'query' elif option == '--list-languages': ListLanguages() else: raise SystemExit("internal error: bad option '%s'" % option) if not options.output: options.output = default_output_fmt() if len(arguments) == 0: raise SystemExit("syntax: %s [options] filename.avi (see --help)" % (sys.argv[0])) if len(arguments) > 1 and options.force_imdb: raise SystemExit("Can't use --force-imdb with multiple files.") if len(arguments) > 1 and isnumber(options.download): raise SystemExit("Can't use --download=ID with multiple files.") return arguments def main(args): files = parseargs(args) no_search_results = 0 for file in files: selected_file = ''; if not os.path.exists(file): raise SystemExit("can't find file '%s'" % file) if options.force_imdb: if options.imdb_id is None: raise SystemExit("With --force-imdb a --imdb-id must be provided.") search_results = SearchSubtitlesByIMDBId(file, options.lang, options.imdb_id) elif options.force_filename: search_results = SearchSubtitlesByFileName(file, options.lang) else: search_results = SearchSubtitlesByHash(file, options.lang) if not search_results and options.imdb_id is not None: print("No results found by hash, trying IMDB id") search_results = SearchSubtitlesByIMDBId(file, options.lang, options.imdb_id) elif not search_results: print("No results found by hash, trying filename") search_results = SearchSubtitlesByFileName(file, options.lang) if not search_results: print("No results found.", file=sys.stderr) no_search_results = no_search_results + 1 continue DisplaySubtitleSearchResults(search_results) if options.download == 'none': raise SystemExit elif options.download == 'first': selected_file = search_results[0] print() print("Defaulting to first result (try --interactive):") DisplaySelectedSubtitle(selected_file) print() AutoDownloadAndSave(file, search_results[0]) elif options.download == 'all': downloaded = {} for search_result in search_results: AutoDownloadAndSave(file, search_result, downloaded) elif options.download == 'query': n = query_num("Enter result to download:", 1, len(search_results)) AutoDownloadAndSave(file, search_results[n-1]) elif options.download == 'best-rating': selected_file = max(search_results, key=lambda sub: float(sub.SubRating)) print() print("Downloading subtitle with best rating:") DisplaySelectedSubtitle(selected_file) print() AutoDownloadAndSave(file, selected_file) elif options.download == 'most-downloaded': selected_file = max(search_results, key=lambda sub: int(sub.SubDownloadsCnt)) print() print("Downloading most downloaded subtitle:") DisplaySelectedSubtitle(selected_file) print() AutoDownloadAndSave(file, selected_file) elif isnumber(options.download): search_result = select_search_result_by_id(options.download, search_results) AutoDownloadAndSave(file, search_result) else: raise Exception("internal error: bad option.download=%s" % options.download) if no_search_results > 0: raise SystemExit("One or more subtitles were not found.") main(sys.argv[1:])