-
-
Notifications
You must be signed in to change notification settings - Fork 65
Installation on macOS
#!/usr/bin/env python3
Copyright (c) 2024 by Emeric GRANGE [email protected]
along with this program. If not, see https://www.gnu.org/licenses/.
import os import sys import time import shutil import struct import argparse import mimetypes import subprocess
import json import urllib import urllib.request import urllib.error
API_URL = 'https://api.opensubtitles.com/api/v1/' API_URL_LOGIN = API_URL + 'login' API_URL_LOGOUT = API_URL + 'logout' API_URL_SEARCH = API_URL + 'subtitles' API_URL_DOWNLOAD = API_URL + 'download'
APP_NAME = 'OpenSubtitlesDownload' APP_VERSION = '6.4' APP_API_KEY = 'FNyoC96mlztsk3ALgNdhfSNapfFY9lOi'
osd_username = 'DPR2' osd_password = 'harutai55'
> Supported language codes: https://opensubtitles.stoplight.io/docs/opensubtitles-api/1de776d20e873-languages
opt_languages = 'en'
opt_language_suffix = 'auto'
opt_language_suffix_separator = '_'
opt_search_mode = 'hash_then_filename'
opt_search_overwrite = True
opt_selection_mode = 'default'
opt_output_path = ''
opt_ignore_hi = False
opt_ignore_machine_translated = True
opt_ignore_ai_translated = False
opt_ignore_foreign_parts_only = False
opt_gui = 'auto'
opt_gui_width = 940 opt_gui_height = 480
Various GUI columns to show/hide during subtitles selection. You can set them to 'on', 'off' or 'auto'.
opt_selection_language = 'auto' opt_selection_match = 'auto' opt_selection_hi = 'auto' opt_selection_fps = 'off' opt_selection_rating = 'off' opt_selection_count = 'off'
custom_command = ""
def checkFileValidity(path):
"""Check mimetype and/or file extension to detect valid video file"""
if os.path.isfile(path) is False:
superPrint("info", "File not found", f"The file provided was not found:
{path}")
return False
fileMimeType, encoding = mimetypes.guess_type(path)
if fileMimeType is None:
fileExtension = path.rsplit('.', 1)
if fileExtension[1] not in ['avi', 'mov', 'mp4', 'mp4v', 'm4v', 'mkv', 'mk3d', 'webm', \
'ts', 'mts', 'm2ts', 'ps', 'vob', 'evo', 'mpeg', 'mpg', \
'asf', 'wm', 'wmv', 'rm', 'rmvb', 'divx', 'xvid']:
#superPrint("error", "File type error!", f"This file is not a video (unknown mimetype AND invalid file extension):<br><i>{path}</i>")
return False
else:
fileMimeType = fileMimeType.split('/', 1)
if fileMimeType[0] != 'video':
#superPrint("error", "File type error!", f"This file is not a video (unknown mimetype):<br><i>{path}</i>")
return False
return True
def checkSubtitlesExists(path): """Check if a subtitles already exists for the current file""" extList = ['srt', 'sub', 'mpl', 'webvtt', 'dfxp', 'txt', 'sbv', 'smi', 'ssa', 'ass', 'usf'] sepList = ['_', '-', '.'] tryList = ['']
if opt_language_suffix_separator not in sepList:
sepList.append(opt_language_suffix_separator)
if opt_language_suffix in ('on', 'auto'):
for language in languageList:
for sep in sepList:
tryList.append(sep + language)
for ext in extList:
for teststring in tryList:
subPath = path.rsplit('.', 1)[0] + teststring + '.' + ext
if os.path.isfile(subPath) is True:
superPrint("info", "Subtitles already downloaded!", f"A subtitles file already exists for this file:<br><i>{subPath}</i>")
return True
return False
This particular implementation is coming from SubDownloader: https://subdownloader.net
def hashFile(path): """Produce a hash for a video file: size + 64bit chksum of the first and last 64k (even if they overlap because the file is smaller than 128k)""" try: longlongformat = 'Q' # unsigned long long little endian bytesize = struct.calcsize(longlongformat) fmt = "<%d%s" % (65536//bytesize, longlongformat)
f = open(path, "rb")
filesize = os.fstat(f.fileno()).st_size
filehash = filesize
if filesize < 65536 * 2:
superPrint("error", "File size error!", f"File size error while generating hash for this file:<br><i>{path}</i>")
return "SizeError"
buf = f.read(65536)
longlongs = struct.unpack(fmt, buf)
filehash += sum(longlongs)
f.seek(-65536, os.SEEK_END) # size is always > 131072
buf = f.read(65536)
longlongs = struct.unpack(fmt, buf)
filehash += sum(longlongs)
filehash &= 0xFFFFFFFFFFFFFFFF
f.close()
returnedhash = "%016x" % filehash
return returnedhash
except IOError:
superPrint("error", "I/O error!", f"Input/Output error while generating hash for this file:<br><i>{path}</i>")
return "IOError"
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
def escapeGUI_title(string): if opt_gui != 'cli': string = string.replace('"', '\"') return string
def escapeGUI_zenity(string):
if opt_gui == 'gnome':
string = string.replace('"', '\"')
string = string.replace("'", "\'")
string = string.replace('', '\\')
string = string.replace("&", "&")
return string
def escapeGUI_kdialog(string):
if opt_gui == 'kde':
string = string.replace('"', '\"')
string = string.replace('', '\\')
return string
def escapePath_wget(string):
string = string.replace('"', '\"')
string = string.replace('', '\\')
return string
def superPrint(priority, title, message):
"""Print messages through terminal, zenity or kdialog"""
if opt_gui == 'gnome':
# Adapt to zenity
message = message.replace("
", "\n")
# Escape
message = escapeGUI_zenity(message)
# Print message
subprocess.call(['zenity', '--width=' + str(opt_gui_width), f'--{priority}', f'--title={title}', f'--text={message}'])
elif opt_gui == 'kde':
# Adapt to kdialog
message = message.replace("\n", "
")
if priority == 'warning':
priority = 'sorry'
elif priority == 'info':
priority = 'msgbox'
# Print message
subprocess.call(['kdialog', '--geometry=' + str(opt_gui_width-220) + 'x' + str(opt_gui_height-128) + '+128+128', f'--title={title}', f'--{priority}={message}'])
else:
# Clean up format tags and line breaks
message = message.replace("\n\n", "\n")
message = message.replace("
", "\n")
message = message.replace("
", "\n")
message = message.replace("", "")
message = message.replace("", "")
message = message.replace("", "")
message = message.replace("", "")
# Print message
print(">> " + message)
def selectionGnome(subtitlesResultList): """GNOME subtitles selection window using zenity""" subtitlesSelectedName = u'' subtitlesSelectedIndex = -1
subtitlesItems = u''
subtitlesMatchedByHash = 0
subtitlesMatchedByName = 0
columnHi = ''
columnLn = ''
columnMatch = ''
columnRate = ''
columnCount = ''
columnFPS = ''
videoTitle_window = escapeGUI_title(videoTitle)
videoTitle_escaped = escapeGUI_zenity(videoTitle)
videoFileName_escaped = escapeGUI_zenity(videoFileName)
# Generate selection window content
for idx, item in enumerate(subtitlesResultList['data']):
if opt_ignore_hi and item['attributes'].get('hearing_impaired', False) == True:
continue
if opt_ignore_foreign_parts_only and item['attributes'].get('foreign_parts_only', False) == True:
continue
if opt_ignore_ai_translated and item['attributes'].get('ai_translated', False) == True:
continue
if opt_ignore_machine_translated and item['attributes'].get('machine_translated', False) == True:
continue
if item['attributes'].get('moviehash_match', False) == True:
subtitlesMatchedByHash += 1
else:
subtitlesMatchedByName += 1
subtitlesItems += f'{idx} "' + escapeGUI_zenity(item['attributes']['files'][0]['file_name']) + '" '
if opt_selection_hi == 'on':
columnHi = '--column="HI" '
if item['attributes'].get('hearing_impaired', False) == True:
subtitlesItems += u'"✔" '
else:
subtitlesItems += '"" '
if opt_selection_language == 'on':
columnLn = '--column="Language" '
subtitlesItems += '"' + item['attributes']['language'] + '" '
if opt_selection_match == 'on':
columnMatch = '--column="MatchedBy" '
if item['attributes'].get('moviehash_match', False) == True:
subtitlesItems += '"HASH" '
else:
subtitlesItems += '"name" '
if opt_selection_rating == 'on':
columnRate = '--column="Rating" '
subtitlesItems += '"' + str(item['attributes']['ratings']) + '" '
if opt_selection_count == 'on':
columnCount = '--column="Downloads" '
subtitlesItems += '"' + str(item['attributes']['download_count']).zfill(5) + '" '
if opt_selection_fps == 'on':
columnFPS = '--column="FPS" '
subtitlesItems += '"' + str(item['attributes']['fps']) + '" '
if subtitlesMatchedByName == 0:
tilestr = f' --title="Subtitles for: {videoTitle_window}" '
textstr = f' --text="<b>Video title:</b> {videoTitle_escaped}\n<b>File name:</b> {videoFileName_escaped}" '
elif subtitlesMatchedByHash == 0:
tilestr = ' --title="Subtitles Search" '
textstr = f' --text="Search results using file name, NOT video detection. <b>May be unreliable...</b>\n<b>File name:</b> {videoFileName_escaped}" '
else: # a mix of the two
tilestr = f' --title="Subtitles for: {videoTitle_window}" '
textstr = f' --text="Search results using file name AND video detection.\n<b>Video title:</b> {videoTitle_escaped}\n<b>File name:</b> {videoFileName_escaped}" '
# Spawn zenity "list" dialog
process_subtitlesSelection = subprocess.Popen('zenity --width=' + str(opt_gui_width) + ' --height=' + str(opt_gui_height) + ' --list' + tilestr + textstr + ' --column "id" --hide-column=1 ' +
'--column="Available subtitles" ' + columnHi + columnLn + columnMatch + columnRate + columnCount + columnFPS + subtitlesItems + ' --print-column=ALL',
shell=True, stdout=subprocess.PIPE)
# Get back the user's choice
result_subtitlesSelection = process_subtitlesSelection.communicate()
# The results contain a subtitles?
if result_subtitlesSelection[0]:
result = str(result_subtitlesSelection[0], 'utf-8', 'replace').strip("\n")
# Get index and result
[subtitlesSelectedIndex, subtitlesSelectedName] = result.split('|')[0:2]
else:
if process_subtitlesSelection.returncode == 0:
subtitlesSelectedName = subtitlesResultList['data'][0]['attributes']['files'][0]['file_name']
subtitlesSelectedIndex = 0
# Return the result (selected subtitles name and index)
return (subtitlesSelectedName, subtitlesSelectedIndex)
def selectionKDE(subtitlesResultList): """KDE subtitles selection window using kdialog""" subtitlesSelectedName = u'' subtitlesSelectedIndex = -1
subtitlesItems = u''
subtitlesMatchedByHash = 0
subtitlesMatchedByName = 0
videoTitle_window = videoTitle
videoTitle_escaped = videoTitle
videoFileName_escaped = escapeGUI_kdialog(videoFileName)
# Generate selection window content
# TODO doesn't support additional columns
index = 0
for idx, item in enumerate(subtitlesResultList['data']):
if opt_ignore_hi and item['attributes'].get('hearing_impaired', False) == True:
continue
if opt_ignore_foreign_parts_only and item['attributes'].get('foreign_parts_only', False) == True:
continue
if opt_ignore_ai_translated and item['attributes'].get('ai_translated', False) == True:
continue
if opt_ignore_machine_translated and item['attributes'].get('machine_translated', False) == True:
continue
if item['attributes'].get('moviehash_match', False) == True:
subtitlesMatchedByHash += 1
else:
subtitlesMatchedByName += 1
# key + subtitles name
subtitlesItems += str(index) + ' "' + item['attributes']['files'][0]['file_name'] + '" '
index += 1
if subtitlesMatchedByName == 0:
tilestr = f' --title="Subtitles for {videoTitle_window}" '
menustr = f' --menu="<b>Video title:</b> {videoTitle_escaped}<br><b>File name:</b> {videoFileName_escaped}" '
elif subtitlesMatchedByHash == 0:
tilestr = ' --title="Subtitles Search" '
menustr = f' --menu="Search results using file name, NOT video detection. <b>May be unreliable...</b><br><b>File name:</b> {videoFileName_escaped}" '
else: # a mix of the two
tilestr = f' --title="Subtitles for {videoTitle_window}" '
menustr = f' --menu="Search results using file name AND video detection.<br><b>Video title:</b> {videoTitle_escaped}<br><b>File name:</b> {videoFileName_escaped}" '
# Spawn kdialog "radiolist"
process_subtitlesSelection = subprocess.Popen('kdialog --geometry=' + str(opt_gui_width-220) + 'x' + str(opt_gui_height-128) + f'+128+128 {tilestr} {menustr} {subtitlesItems}',
shell=True, stdout=subprocess.PIPE)
# Get back the user's choice
result_subtitlesSelection = process_subtitlesSelection.communicate()
# The results contain the key matching a subtitles?
if result_subtitlesSelection[0]:
subtitlesSelectedIndex = int(str(result_subtitlesSelection[0], 'utf-8', 'replace').strip("\n"))
subtitlesSelectedName = subtitlesResultList['data'][subtitlesSelectedIndex]['attributes']['files'][0]['file_name']
# Return the result (selected subtitles name and index)
return (subtitlesSelectedName, subtitlesSelectedIndex)
def selectionCLI(subtitlesResultList): """Command Line Interface, subtitles selection inside your current terminal""" subtitlesSelectedName = u'' subtitlesSelectedIndex = -1
subtitlesMatchedByHash = 0
subtitlesMatchedByName = 0
# Check if search has results by hash or name
for item in subtitlesResultList['data']:
if item['attributes'].get('moviehash_match', False) == True:
subtitlesMatchedByHash += 1
else:
subtitlesMatchedByName += 1
# Print video infos
if subtitlesMatchedByName == 0:
print("\n>> Subtitles for: " + videoTitle)
elif subtitlesMatchedByHash == 0:
print("\n>> Subtitles for file: " + videoFileName)
print(">> Search results using file name, NOT video detection. May be unreliable...")
else: # a mix of the two
print("\n>> Subtitles for: " + videoTitle)
print(">> Search results using using file name AND video detection.")
print("\n>> Available subtitles:")
# Print subtitles list on the terminal
for idx, item in enumerate(subtitlesResultList['data']):
if opt_ignore_hi and item['attributes'].get('hearing_impaired', False) == True:
continue
if opt_ignore_foreign_parts_only and item['attributes'].get('foreign_parts_only', False) == True:
continue
if opt_ignore_ai_translated and item['attributes'].get('ai_translated', False) == True:
continue
if opt_ignore_machine_translated and item['attributes'].get('machine_translated', False) == True:
continue
subtitlesItemPre = u'> '
subtitlesItem = u'"' + item['attributes']['files'][0]['file_name'] + u'"'
subtitlesItemPost = u''
if opt_selection_match == 'on':
if item['attributes'].get('moviehash_match', False) == True:
subtitlesItemPre += '(hash) > '
else:
subtitlesItemPre += '(name) > '
if opt_selection_language == 'on':
subtitlesItemPre += item['attributes']['language'].upper() + ' > '
if opt_selection_hi == 'on' and item['attributes'].get('hearing_impaired', False) == True:
subtitlesItemPost += ' > ' + '\033[44m' + ' HI ' + '\033[0m'
if opt_selection_fps == 'on':
subtitlesItemPost += ' > ' + '\033[100m' + str(item['attributes']['fps']) + ' FPS' + '\033[0m'
if opt_selection_rating == 'on':
subtitlesItemPost += ' > ' + '\033[100m' + 'Rating: ' + str(item['attributes']['ratings']) + '\033[0m'
if opt_selection_count == 'on':
subtitlesItemPost += ' > ' + '\033[100m' + 'Downloads: ' + str(item['attributes']['download_count']) + '\033[0m'
# type # season_number # episode_number
if (item['attributes']['feature_details'].get('season_number', 0) != 0 and item['attributes']['feature_details'].get('episode_number', 0) != 0):
subtitlesItemPost += ' > ' + '\033[100m' + 'S' + str(item['attributes']['feature_details']['season_number']).zfill(2) + 'E' + str(item['attributes']['feature_details']['episode_number']).zfill(2) + '\033[0m'
idx += 1 # We display subtitles indexes starting from 1, 0 is reserved for cancel
if item['attributes'].get('moviehash_match', False) == True:
print("\033[92m[" + str(idx).rjust(2, ' ') + "]\033[0m " + subtitlesItemPre + subtitlesItem + subtitlesItemPost)
else:
print("\033[93m[" + str(idx).rjust(2, ' ') + "]\033[0m " + subtitlesItemPre + subtitlesItem + subtitlesItemPost)
# Ask user to selected a subtitles
print("\033[91m[ 0]\033[0m Cancel search")
while (subtitlesSelectedIndex < 0 or subtitlesSelectedIndex > idx):
try:
subtitlesSelectedIndex = int(input("\n>> Enter your choice [0-" + str(idx) + "]: "))
except KeyboardInterrupt:
sys.exit(1)
except:
subtitlesSelectedIndex = -1
if subtitlesSelectedIndex <= 0:
print("Cancelling search...")
return ("", -1)
subtitlesSelectedIndex -= 1
subtitlesSelectedName = subtitlesResultList['data'][subtitlesSelectedIndex]['attributes']['files'][0]['file_name']
# Return the result (selected subtitles name and index)
return (subtitlesSelectedName, subtitlesSelectedIndex)
def selectionAuto(subtitlesResultList, languageList): """Automatic subtitles selection using filename match""" subtitlesSelectedName = u'' subtitlesSelectedIndex = -1
videoFileParts = videoFileName.replace('-', '.').replace(' ', '.').replace('_', '.').lower().split('.')
languageListReversed = list(reversed(languageList))
maxScore = -1
for idx, item in enumerate(subtitlesResultList['data']):
score = 0
# points to respect languages priority
score += languageListReversed.index(item['attributes']['language']) * 100
# extra point if the sub is found by hash
if item['attributes'].get('moviehash_match', False) == True:
score += 1
# points for filename mach
subFileParts = item['attributes']['files'][0]['file_name'].replace('-', '.').replace(' ', '.').replace('_', '.').lower().split('.')
for subPart in subFileParts:
for filePart in videoFileParts:
if subPart == filePart:
score += 1
if score > maxScore:
maxScore = score
subtitlesSelectedIndex = idx
subtitlesSelectedName = subtitlesResultList['data'][subtitlesSelectedIndex]['attributes']['files'][0]['file_name']
# Return the result (selected subtitles name and index)
return (subtitlesSelectedName, subtitlesSelectedIndex)
def pythonChecker(): """Check the availability of Python 3 interpreter""" if sys.version_info < (3, 0): superPrint("error", "Wrong Python version", "You need Python 3 to use OpenSubtitlesDownload.") return False return True
def dependencyChecker(): """Check the availability of tools used as dependencies""" if opt_gui == 'gnome': for tool in ['wget']: path = shutil.which(tool) if path is None: superPrint("error", "Missing dependency!", f"{tool} is not available, please install it!") return False return True
def getUserToken(username, password): try: headers = { "User-Agent": f"{APP_NAME} v{APP_VERSION}", "Api-key": f"{APP_API_KEY}", "Accept": "application/json", "Content-Type": "application/json" } payload = { "username": username, "password": password }
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(API_URL_LOGIN, data=data, headers=headers)
with urllib.request.urlopen(req) as response:
response_data = json.loads(response.read().decode('utf-8'))
#print("getUserToken() response data: " + str(response_data))
return response_data['token']
except (urllib.error.HTTPError, urllib.error.URLError) as err:
print("Urllib error (", err.code, ") ", err.reason)
superPrint("error", "OpenSubtitles.com login error!", "An error occurred while connecting to the OpenSubtitles.com server")
sys.exit(2)
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
superPrint("error", "OpenSubtitles.com login error!", "An error occurred while connecting to the OpenSubtitles.com server")
sys.exit(2)
def destroyUserToken(USER_TOKEN): try: headers = { "User-Agent": f"{APP_NAME} v{APP_VERSION}", "Api-key": f"{APP_API_KEY}", "Authorization": f"Bearer {USER_TOKEN}", "Accept": "application/json", "Content-Type": "application/json" }
req = urllib.request.Request(API_URL_LOGOUT, headers=headers)
with urllib.request.urlopen(req) as response:
response_data = json.loads(response.read().decode('utf-8'))
#print("destroyUserToken() response data: " + str(response_data))
return response_data
except (urllib.error.HTTPError, urllib.error.URLError) as err:
print("Urllib error (", err.code, ") ", err.reason)
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
def searchSubtitles(**kwargs): try: headers = { "User-Agent": f"{APP_NAME} v{APP_VERSION}", "Api-key": f"{APP_API_KEY}" }
query_params = urllib.parse.urlencode(kwargs)
url = f"{API_URL_SEARCH}?{query_params}"
req = urllib.request.Request(url, headers=headers)
with urllib.request.urlopen(req) as response:
response_data = json.loads(response.read().decode('utf-8'))
#print("searchSubtitles() response data: " + str(response_data))
return response_data
except (urllib.error.HTTPError, urllib.error.URLError) as err:
print("Urllib error (", err.code, ") ", err.reason)
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
def getSubtitlesInfo(USER_TOKEN, file_id): try: headers = { "User-Agent": f"{APP_NAME} v{APP_VERSION}", "Api-key": f"{APP_API_KEY}", "Authorization": f"Bearer {USER_TOKEN}", "Accept": "application/json", "Content-Type": "application/json" } payload = { "file_id": file_id }
data = json.dumps(payload).encode('utf-8')
req = urllib.request.Request(API_URL_DOWNLOAD, data=data, headers=headers)
with urllib.request.urlopen(req) as response:
response_data = json.loads(response.read().decode('utf-8'))
#print("getSubtitlesInfo() response data:" + response_data)
return response_data
except (urllib.error.HTTPError, urllib.error.URLError) as err:
print("Urllib error (", err.code, ") ", err.reason)
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
def downloadSubtitles(USER_TOKEN, subURL, subPath): try: headers = { "User-Agent": f"{APP_NAME} v{APP_VERSION}", "Api-key": f"{APP_API_KEY}", "Authorization": f"Bearer {USER_TOKEN}", "Accept": "application/json", "Content-Type": "application/json" }
req = urllib.request.Request(subURL, headers=headers)
with urllib.request.urlopen(req) as response:
decodedStr = response.read().decode('utf-8')
byteswritten = open(subPath, 'w', encoding='utf-8', errors='replace').write(decodedStr)
if byteswritten > 0:
return 0
return 1
except (urllib.error.HTTPError, urllib.error.URLError) as err:
print("Urllib error (", err.code, ") ", err.reason)
except Exception:
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
==============================================================================
==============================================================================
ExitCode = 2
videoPathList = [] languageList = []
currentVideoPath = u"" currentLanguage = u""
if os.path.isabs(sys.argv[0]): scriptPath = sys.argv[0] else: scriptPath = os.getcwd() + "/" + str(sys.argv[0])
parser = argparse.ArgumentParser(prog='OpenSubtitlesDownload.py', description='Automatically find and download the right subtitles for your favorite videos!', formatter_class=argparse.RawTextHelpFormatter)
parser.add_argument('--cli', help="Force CLI mode", action='store_true') parser.add_argument('-g', '--gui', help="Select the GUI you want from: auto, kde, gnome, cli (default: auto)") parser.add_argument('-u', '--username', help="Set opensubtitles.com account username") parser.add_argument('-p', '--password', help="Set opensubtitles.com account password") parser.add_argument('-l', '--lang', help="Specify the language in which the subtitles should be downloaded (default: en).\nSyntax:\n-l en,fr: search in both language") parser.add_argument('-s', '--search', help="Search mode: hash, filename, hash_then_filename, hash_and_filename (default: hash_then_filename)") parser.add_argument('-t', '--select', help="Selection mode: manual, default, auto") parser.add_argument('-a', '--auto', help="Force automatic selection and download of the best subtitles found", action='store_true') parser.add_argument('-i', '--skip', help="Skip search if an existing subtitles file is detected", action='store_true') parser.add_argument('-o', '--output', help="Override subtitles download path, instead of next to their video file") parser.add_argument('-x', '--suffix', help="Force language code file suffix", action='store_true') parser.add_argument('--noai', help="Ignore AI or machine translated subtitles", action='store_true') parser.add_argument('--nohi', help="Ignore HI (hearing impaired) subtitles", action='store_true') parser.add_argument('searchPathList', help="The video file(s) or folder(s) for which subtitles should be searched and downloaded", nargs='+') arguments = parser.parse_args()
if arguments.cli: opt_gui = 'cli' if arguments.gui: opt_gui = arguments.gui if arguments.username and arguments.password: osd_username = arguments.username osd_password = arguments.password if arguments.lang: opt_languages = arguments.lang if arguments.search: opt_search_mode = arguments.search if arguments.skip: opt_search_overwrite = False if arguments.select: opt_selection_mode = arguments.select if arguments.auto: opt_selection_mode = 'auto' if arguments.output: opt_output_path = arguments.output if arguments.suffix: opt_language_suffix = 'on' if arguments.noai: opt_ignore_ai_translated = True if arguments.nohi: opt_ignore_hi = True
if opt_gui == 'auto': # Note: "ps cax" only output the first 15 characters of the executable's names ps = str(subprocess.Popen(['ps', 'cax'], stdout=subprocess.PIPE).communicate()[0]).split('\n') for line in ps: if ('gnome-session' in line) or ('cinnamon-sessio' in line) or ('mate-session' in line) or ('xfce4-session' in line): opt_gui = 'gnome' break elif 'ksmserver' in line: opt_gui = 'kde' break
if opt_gui not in ['gnome', 'kde', 'cli']: opt_gui = 'cli' opt_search_mode = 'hash_then_filename' opt_selection_mode = 'auto' print("Unknown GUI, falling back to an automatic CLI mode")
if opt_search_mode not in ['hash', 'filename', 'hash_then_filename', 'hash_and_filename']: opt_search_mode = 'hash_then_filename'
if opt_selection_mode not in ['manual', 'default', 'auto']: opt_selection_mode = 'default'
if pythonChecker() is False: sys.exit(2)
if dependencyChecker() is False: sys.exit(2)
if not osd_username or not osd_password: superPrint("warning", "OpenSubtitles.com account required!", "A valid account from OpenSubtitles.com is REQUIRED, please register on the website!") sys.exit(2)
if isinstance(opt_languages, list): languageList = opt_languages else: languageList = opt_languages.split(',')
languageCount_search = len(languageList)
for i in arguments.searchPathList: path = os.path.abspath(i) if os.path.isdir(path): # if it's a folder if opt_gui == 'cli': # check all of the folder's (recursively) for root, _, items in os.walk(path): for item in items: localPath = os.path.join(root, item) if checkFileValidity(localPath): if opt_search_overwrite or (not opt_search_overwrite and not checkSubtitlesExists(localPath)): videoPathList.append(localPath) else: # check all of the folder's files for item in os.listdir(path): localPath = os.path.join(path, item) if checkFileValidity(localPath): if opt_search_overwrite or (not opt_search_overwrite and not checkSubtitlesExists(localPath)): videoPathList.append(localPath) elif checkFileValidity(path): # if it is a file if opt_search_overwrite or (not opt_search_overwrite and not checkSubtitlesExists(path)): videoPathList.append(path)
if not videoPathList: sys.exit(1)
currentVideoPath = videoPathList[0] videoPathList.pop(0)
for videoPathDispatch in videoPathList:
# Pass settings
command = [ sys.executable, scriptPath,
"-g", opt_gui, "-s", opt_search_mode, "-t", opt_selection_mode, "-l", opt_languages ]
if not opt_search_overwrite:
command.append("-i")
if opt_language_suffix == 'on':
command.append("-x")
if opt_output_path:
command.append("-o")
command.append(opt_output_path)
if arguments.username and arguments.password:
command.append("-u")
command.append(arguments.username)
command.append("-p")
command.append(arguments.password)
# Pass video file
command.append(videoPathDispatch)
# Do not spawn too many instances at once, avoid error '429 Too Many Requests'
time.sleep(2)
if opt_gui == 'cli' and opt_selection_mode != 'auto':
# Synchronous call
process_videoDispatched = subprocess.call(command)
else:
# Asynchronous call
process_videoDispatched = subprocess.Popen(command)
try: USER_TOKEN = [] subtitlesResultList = [] languageCount_results = 0
## Get file hash, size and name
videoTitle = u''
videoHash = hashFile(currentVideoPath)
videoSize = os.path.getsize(currentVideoPath)
videoFileName = os.path.basename(currentVideoPath)
## Search for subtitles
try:
if (opt_search_mode == 'hash_and_filename'):
subtitlesResultList = searchSubtitles(moviehash=videoHash, query=videoFileName, languages=opt_languages)
#print(f"SEARCH BY HASH AND NAME >>>>> length {len(subtitlesResultList['data'])} >>>>> {subtitlesResultList['data']}")
else:
if any(mode in opt_search_mode for mode in ['hash_then_filename', 'hash']):
subtitlesResultList = searchSubtitles(moviehash=videoHash, languages=opt_languages)
#print(f"SEARCH BY HASH >>>>> length {len(subtitlesResultList['data'])} >>>>> {subtitlesResultList['data']}")
if ((opt_search_mode == 'filename') or
(opt_search_mode == 'hash_then_filename' and len(subtitlesResultList['data']) == 0)):
subtitlesResultList = searchSubtitles(query=videoFileName, languages=opt_languages)
#print(f"SEARCH BY NAME >>>>> length {len(subtitlesResultList['data'])} >>>>> {subtitlesResultList['data']}")
except Exception:
superPrint("error", "Search error!", "Unable to reach opensubtitles.com servers!<br><b>Search error</b>")
sys.exit(2)
## Parse the results of the search query
if subtitlesResultList and 'data' in subtitlesResultList and len(subtitlesResultList['data']) > 0:
# Mark search as successful
languageCount_results += 1
subName = u''
subIndex = 0
# If there is only one subtitles (matched by file hash), auto-select it (except in CLI mode)
if (len(subtitlesResultList['data']) == 1) and (subtitlesResultList['data'][0]['attributes'].get('moviehash_match', False) == True):
if opt_selection_mode != 'manual':
subName = subtitlesResultList['data'][0]['attributes']['files'][0]['file_id']
# Check if we have a valid title, found by hash
for item in subtitlesResultList['data']:
if item['attributes'].get('moviehash_match', False) == True:
videoTitle = item['attributes']['feature_details']['movie_name']
break
# If there is more than one subtitles and opt_selection_mode != 'auto',
# then let the user decide which one will be downloaded
if not subName:
if opt_selection_mode == 'auto':
# Automatic subtitles selection
(subName, subIndex) = selectionAuto(subtitlesResultList, languageList)
else:
# Go through the list of subtitles and handle 'auto' settings activation
for item in subtitlesResultList['data']:
if opt_selection_match == 'auto':
if (opt_search_mode == 'hash_and_filename' or opt_search_mode == 'hash_then_filename'):
if item['attributes'].get('moviehash_match', False) == False:
opt_selection_match = 'on'
if opt_selection_language == 'auto' and languageCount_search > 1:
opt_selection_language = 'on'
if opt_selection_hi == 'auto' and item['attributes'].get('hearing_impaired', False) == True:
opt_selection_hi = 'on'
if opt_selection_rating == 'auto' and item['attributes']['ratings'] != '0.0':
opt_selection_rating = 'on'
if opt_selection_count == 'auto':
opt_selection_count = 'on'
if opt_selection_fps == 'auto' and item['attributes'].get('fps', '0.0') != '0.0':
opt_selection_fps = 'on'
# Spaw selection window
if opt_gui == 'gnome':
(subName, subIndex) = selectionGnome(subtitlesResultList)
elif opt_gui == 'kde':
(subName, subIndex) = selectionKDE(subtitlesResultList)
else: # CLI
(subName, subIndex) = selectionCLI(subtitlesResultList)
## At this point a subtitles should be selected
if subName:
# Log-in to the API
USER_TOKEN = getUserToken(username=osd_username, password=osd_password)
# Prepare download
fileId = subtitlesResultList['data'][int(subIndex)]['attributes']['files'][0]['file_id']
fileInfo = getSubtitlesInfo(USER_TOKEN, fileId)
# Quote the URL to avoid characters like brackets () causing errors in wget command below
subURL = fileInfo['link']
subSuffix = subURL.split('.')[-1].strip("'")
subLangName = subtitlesResultList['data'][int(subIndex)]['attributes']['language']
subPath = u''
if opt_output_path and os.path.isdir(os.path.abspath(opt_output_path)):
# Use the output path provided by the user
subPath = os.path.abspath(opt_output_path) + "/" + currentVideoPath.rsplit('.', 1)[0].rsplit('/', 1)[1] + '.' + subSuffix
else:
# Use the path of the input video, and the suffix of the subtitles file
subPath = currentVideoPath.rsplit('.', 1)[0] + '.' + subSuffix
# Write language code into the filename?
if opt_language_suffix == 'on':
subPath = subPath.rsplit('.', 1)[0] + opt_language_suffix_separator + subtitlesResultList['data'][int(subIndex)]['attributes']['language'] + '.' + subSuffix
# Empty videoTitle? Use filename
if not videoTitle:
videoTitle = videoFileName
## Download and unzip the selected subtitles
if opt_gui == 'gnome':
# Escape non-alphanumeric characters from the subtitles download path for wget, and video title for zenity
subPathEscaped = escapePath_wget(subPath)
videoTitleEscaped = escapeGUI_zenity(videoTitle)
# Download with wget, piped into zenity --progress
process_subtitlesDownload = subprocess.call(f'(wget -q -O "{subPathEscaped}" "{subURL}") 2>&1 ' +
'| (zenity --auto-close --progress --pulsate --title="Downloading subtitles, please wait..." ' +
f'--text="Downloading <b>{subLangName}</b> subtitles for <b>{videoTitleEscaped}</b>...")', shell=True)
else:
if opt_gui == 'cli':
print(f">> Downloading '{subLangName}' subtitles for '{videoTitle}'")
process_subtitlesDownload = downloadSubtitles(USER_TOKEN, fileInfo['link'], subPath)
# If an error occurs, say so
if process_subtitlesDownload != 0:
superPrint("error", "Subtitling error!", f"An error occurred while downloading or writing <b>{subLangName}</b> subtitles for <b>{videoTitle}</b>.")
sys.exit(2)
## HOOK # Use a secondary tool on the subtitles file after a successful download?
if process_subtitlesDownload == 0 and len(custom_command) > 0:
subPathEscaped = escapePath_wget(subPath)
process_subtitlesDownload = subprocess.call(f'{custom_command} "{subPathEscaped}"', shell=True)
## Print a message if no subtitles have been found, for any of the languages
if languageCount_results == 0:
superPrint("info", "No subtitles available :-(", f"<b>No subtitles found</b> for this video:<br><i>{videoFileName}</i>")
ExitCode = 1
else:
ExitCode = 0
except KeyboardInterrupt: sys.exit(1)
except urllib.error.HTTPError as e: superPrint("error", "Network error", "Network error: " + e.reason)
except (OSError, IOError, RuntimeError, AttributeError, TypeError, NameError, KeyError):
# An unknown error occur, let's apologize before exiting
superPrint("error", "Unexpected error!",
"OpenSubtitlesDownload encountered an unknown error, sorry about that...
" +
"Error: " + str(sys.exc_info()[0]).replace('<', '[').replace('>', ']') + "
" +
"Line: " + str(sys.exc_info()[-1].tb_lineno) + "
" +
"Just to be safe, please check:
" +
"- Your Internet connection status
" +
"- www.opensubtitles.com availability
" +
"- Your download limits (10 subtitles per 24h for non VIP users)
" +
"- That are using the latest version of this software ;-)")
except Exception: # Catch unhandled exceptions but do not spawn an error window print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))