Created
December 29, 2023 13:16
-
-
Save emericg/d47eeed36c7923f6147e7b6c641f0cee to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
# OpenSubtitlesDownload.py / Version 6.0 | |
# This software is designed to help you find and download subtitles for your favorite videos! | |
# You can browse the project's GitHub page: | |
# - https://github.com/emericg/OpenSubtitlesDownload | |
# Learn much more about customizing OpenSubtitlesDownload.py for your needs: | |
# - https://github.com/emericg/OpenSubtitlesDownload/wiki | |
# Copyright (c) 2024 by Emeric GRANGE <emeric.grange@gmail.com> | |
# | |
# This program is free software: you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation, either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <https://www.gnu.org/licenses/>. | |
import os | |
import re | |
import sys | |
import time | |
import shutil | |
import struct | |
import argparse | |
import mimetypes | |
import subprocess | |
import json | |
import urllib | |
from urllib import request | |
# ==== OpenSubtitles.com server settings ======================================= | |
# Track API availability: | |
# > https://opensubtitles.stoplight.io/docs/opensubtitles-api/e3750fd63a100-getting-started#system-status | |
# API endpoints | |
API_URL = 'https://api.opensubtitles.com/api/v1/' | |
API_URL_DOWNLOAD_ENDPOINT = API_URL + 'download' | |
API_URL_LOGIN_ENDPOINT = API_URL + 'login' | |
API_URL_SEARCH_ENDPOINT = API_URL + 'subtitles' | |
# API key (required) | |
API_KEY = '9noeoxtKzopv6auSNXVvVNuh6ukqFKyC' | |
# ==== OpenSubtitles.com account (required) ==================================== | |
# You can use your opensubtitles.com VIP account to avoid "in-subtitles" advertisement and bypass download limits. | |
# Be careful about your password security, it will be stored right here in plain text... | |
# You can also change opensubtitles.com language, it will be used for error codes and stuff. | |
# Can be overridden at run time with '-u' and '-p' arguments. | |
osd_username = '' | |
osd_password = '' | |
osd_language = 'en' | |
# ==== Language settings ======================================================= | |
# Full guide: https://github.com/emericg/OpenSubtitlesDownload/wiki/Adjust-settings | |
# 1/ Change the search language by using any supported 2-letter (ISO 639-1) language code: | |
# > https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes | |
# > Supported language codes: https://opensubtitles.stoplight.io/docs/opensubtitles-api/1de776d20e873-languages | |
# > Ex: opt_languages = 'en' | |
# 2/ Search for subtitles in several languages (at once, select one) by using multiple codes separated by a comma: | |
# > Ex: opt_languages = 'en,fr' | |
opt_languages = 'en' | |
# Write language code (ex: _en) at the end of the subtitles file. 'on', 'off' or 'auto'. | |
# If you are regularly searching for several language at once, you sould use 'on'. | |
opt_language_suffix = 'auto' | |
# Character used to separate file path from the language code (ex: file_en.srt). | |
opt_language_suffix_separator = '_' | |
# Force downloading and storing UTF-8 encoded subtitles files. | |
opt_force_utf8 = False | |
# ==== Search settings ========================================================= | |
# Subtitles search mode. Can be overridden at run time with '-s' argument. | |
# - hash (search by hash only) | |
# - filename (search by filename only) | |
# - hash_then_filename (search by hash, then if no results by filename) | |
# - hash_and_filename (search using both methods) | |
opt_search_mode = 'hash_then_filename' | |
# Search and download a subtitles even if a subtitles file already exists. | |
opt_search_overwrite = True | |
# Subtitles selection mode. Can be overridden at run time with '-t' argument. | |
# - default (in case of multiple results, let you choose the subtitles you want) | |
# - manual (always let you choose the subtitles you want) | |
# - auto (automatically select the best subtitles found) | |
opt_selection_mode = 'default' | |
# Customize subtitles download path. Can be overridden at run time with '-o' argument. | |
# By default, subtitles are downloaded next to their video file. | |
opt_output_path = '' | |
# ==== GUI settings ============================================================ | |
# Select your GUI. Can be overridden at run time with '--gui=xxx' argument. | |
# - auto (autodetection, fallback on CLI) | |
# - gnome (GNOME/GTK based environments, using 'zenity' backend) | |
# - kde (KDE/Qt based environments, using 'kdialog' backend) | |
# - cli (Command Line Interface) | |
opt_gui = 'auto' | |
# Change the subtitles selection GUI size: | |
opt_gui_width = 920 | |
opt_gui_height = 400 | |
# Various GUI columns to show/hide during subtitles selection. You can set them to 'on', 'off' or 'auto'. | |
opt_selection_hi = 'auto' | |
opt_selection_language = 'auto' | |
opt_selection_match = 'auto' | |
opt_selection_rating = 'off' | |
opt_selection_count = 'off' | |
# ==== Super Print ============================================================= | |
# priority: info, warning, error | |
# title: only for zenity and kdialog messages | |
# message: full text, with tags and breaks (tags will be cleaned up for CLI) | |
def superPrint(priority, title, message): | |
"""Print messages through terminal, zenity or kdialog""" | |
if opt_gui == 'gnome': | |
subprocess.call(['zenity', '--width=' + str(opt_gui_width), '--' + priority, '--title=' + title, '--text=' + message]) | |
elif opt_gui == 'kde': | |
# Adapt to kdialog | |
message = message.replace("\n", "<br>") | |
message = message.replace('\\"', '"') | |
if priority == 'warning': | |
priority = 'sorry' | |
elif priority == 'info': | |
priority = 'msgbox' | |
# Print message | |
subprocess.call(['kdialog', '--geometry=' + str(opt_gui_width) + 'x' + str(opt_gui_height), '--title=' + title, '--' + priority + '=' + message]) | |
else: | |
# Clean up format tags from the zenity string | |
message = message.replace("\n\n", "\n") | |
message = message.replace('\\"', '"') | |
message = message.replace("<i>", "") | |
message = message.replace("</i>", "") | |
message = message.replace("<b>", "") | |
message = message.replace("</b>", "") | |
# Print message | |
print(">> " + message) | |
# ==== Check file path & type ================================================== | |
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", "The file provided was not found:\n<i>" + path + "</i>") | |
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!", "This file is not a video (unknown mimetype AND invalid file extension):\n<i>" + path + "</i>") | |
return False | |
else: | |
fileMimeType = fileMimeType.split('/', 1) | |
if fileMimeType[0] != 'video': | |
#superPrint("error", "File type error!", "This file is not a video (unknown mimetype):\n<i>" + path + "</i>") | |
return False | |
return True | |
# ==== Check for existing subtitles file ======================================= | |
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'] | |
lngList = [''] | |
if opt_language_suffix in ('on', 'auto'): | |
for language in opt_languages: | |
for l in list(language.split(',')): | |
lngList.append(opt_language_suffix_separator + l) | |
for ext in extList: | |
for lng in lngList: | |
subPath = path.rsplit('.', 1)[0] + lng + '.' + ext | |
if os.path.isfile(subPath) is True: | |
superPrint("info", "Subtitles already downloaded!", "A subtitles file already exists for this file:\n<i>" + subPath + "</i>") | |
return True | |
return False | |
# ==== Hashing algorithm ======================================================= | |
# Info: https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes | |
# 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!", "File size error while generating hash for this file:\n<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!", "Input/Output error while generating hash for this file:\n<i>" + path + "</i>") | |
return "IOError" | |
except Exception: | |
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0])) | |
# ==== GNOME selection window ================================================== | |
def selectionGnome(subtitlesResultList): | |
"""GNOME subtitles selection window using zenity""" | |
subtitlesSelectedName = u'' | |
subtitlesSelectedIndex = -1 | |
subtitlesItems = u'' | |
subtitlesMatchedByHash = 0 | |
subtitlesMatchedByName = 0 | |
columnHi = '' | |
columnLn = '' | |
columnMatch = '' | |
columnRate = '' | |
columnCount = '' | |
# Generate selection window content | |
for idx, item in enumerate(subtitlesResultList['data']): | |
if item['attributes'].get('moviehash_match', False) == True: | |
subtitlesMatchedByHash += 1 | |
else: | |
subtitlesMatchedByName += 1 | |
subtitlesItems += f'{idx} "' + item['attributes']['files'][0]['file_name'] + '" ' | |
if opt_selection_hi == 'on': | |
columnHi = '--column="HI" ' | |
if item['attributes']['hearing_impaired'] == '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']['moviehash_match'] == 'True': | |
subtitlesItems += '"HASH" ' | |
else: | |
subtitlesItems += '"" ' | |
if opt_selection_rating == 'on': | |
columnRate = '--column="Rating" ' | |
subtitlesItems += '"' + item['attributes']['ratings'] + '" ' | |
if opt_selection_count == 'on': | |
columnCount = '--column="Downloads" ' | |
subtitlesItems += '"' + item['attributes']['download_count'] + '" ' | |
if subtitlesMatchedByName == 0: | |
tilestr = ' --title="Subtitles for: ' + videoTitle + '"' | |
textstr = ' --text="<b>Video title:</b> ' + videoTitle + '\n<b>File name:</b> ' + videoFileName + '"' | |
elif subtitlesMatchedByHash == 0: | |
tilestr = ' --title="Subtitles for: ' + videoFileName + '"' | |
textstr = ' --text="Search results using file name, NOT video detection. <b>May be unreliable...</b>\n<b>File name:</b> ' + videoFileName + '" ' | |
else: # a mix of the two | |
tilestr = ' --title="Subtitles for: ' + videoTitle + '"' | |
textstr = ' --text="Search results using file name AND video detection.\n<b>Video title:</b> ' + videoTitle + '\n<b>File name:</b> ' + videoFileName + '"' | |
# Spawn zenity "list" dialog | |
process_subtitlesSelection = subprocess.Popen('zenity --width=' + str(opt_gui_width) + ' --height=' + str(opt_gui_height) + ' --list' + tilestr + textstr | |
+ ' --column "id" --column="Available subtitles" ' + columnHi + columnLn + columnMatch + columnRate + columnCount + subtitlesItems | |
+ ' --hide-column=1 --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") | |
# Hack against recent zenity version? | |
#if len(result.split("|")) > 1: | |
# if result.split("|")[0] == result.split("|")[1]: | |
# result = result.split("|")[0] | |
# 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) | |
# ==== KDE selection window ==================================================== | |
def selectionKDE(subtitlesResultList): | |
"""KDE subtitles selection window using kdialog""" | |
subtitlesSelectedName = u'' | |
subtitlesSelectedIndex = -1 | |
subtitlesItems = u'' | |
subtitlesMatchedByHash = 0 | |
subtitlesMatchedByName = 0 | |
# Generate selection window content | |
# TODO doesn't support additional columns | |
index = 0 | |
for item in subtitlesResultList['data']: | |
if item['attributes']['moviehash_match'] == 'True': | |
subtitlesMatchedByHash += 1 | |
else: | |
subtitlesMatchedByName += 1 | |
# key + subtitles name | |
subtitlesItems += str(index) + ' "' + item['attributes']['files'][0]['file_name'] + '" ' | |
index += 1 | |
if subtitlesMatchedByName == 0: | |
tilestr = ' --title="Subtitles for ' + videoTitle + '"' | |
menustr = ' --menu="<b>Video title:</b> ' + videoTitle + '<br><b>File name:</b> ' + videoFileName + '" ' | |
elif subtitlesMatchedByHash == 0: | |
tilestr = ' --title="Subtitles for ' + videoFileName + '"' | |
menustr = ' --menu="Search results using file name, NOT video detection. <b>May be unreliable...</b><br><b>File name:</b> ' + videoFileName + '" ' | |
else: # a mix of the two | |
tilestr = ' --title="Subtitles for ' + videoTitle + '" ' | |
menustr = ' --menu="Search results using file name AND video detection.<br><b>Video title:</b> ' + videoTitle + '<br><b>File name:</b> ' + videoFileName + '" ' | |
# Spawn kdialog "radiolist" | |
process_subtitlesSelection = subprocess.Popen('kdialog --geometry=' + str(opt_gui_width) + 'x' + str(opt_gui_height) + 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) | |
# ==== CLI selection mode ====================================================== | |
def selectionCLI(subtitlesResultList): | |
"""Command Line Interface, subtitles selection inside your current terminal""" | |
subtitlesSelectedName = u'' | |
subtitlesSelectedIndex = -1 | |
subtitlesItemIndex = 0 | |
subtitlesItem = u'' | |
# Print video infos | |
print("\n>> Title: " + videoTitle) | |
print(">> Filename: " + videoFileName) | |
# Print subtitles list on the terminal | |
print(">> Available subtitles:") | |
for item in subtitlesResultList['data']: | |
subtitlesItemIndex += 1 | |
subtitlesItem = '"' + item['attributes']['files'][0]['file_name'] + '" ' | |
if opt_selection_hi == 'on' and item['attributes']['hearing_impaired'] == '1': | |
subtitlesItem += '> "HI" ' | |
if opt_selection_language == 'on': | |
subtitlesItem += '> "Language: ' + item['attributes']['language'] + '" ' | |
if opt_selection_match == 'on': | |
if item['attributes']['moviehash_match'] == 'True': | |
subtitlesItem += '> "Matched by hash" ' | |
else: | |
subtitlesItem += '> "Matched by name" ' | |
if opt_selection_rating == 'on': | |
subtitlesItem += '> "SubRating: ' + item['attributes']['ratings'] + '" ' | |
if opt_selection_count == 'on': | |
subtitlesItem += '> "SubDownloadsCnt: ' + item['attributes']['download_count'] + '" ' | |
if "moviehash_match" in item['attributes'] and item['attributes']['moviehash_match'] == 'True': | |
print("\033[92m[" + str(subtitlesItemIndex) + "]\033[0m " + subtitlesItem) | |
else: | |
print("\033[93m[" + str(subtitlesItemIndex) + "]\033[0m " + subtitlesItem) | |
# Ask user to selected a subtitles | |
print("\033[91m[0]\033[0m Cancel search") | |
while (subtitlesSelectedIndex < 0 or subtitlesSelectedIndex > subtitlesItemIndex): | |
try: | |
subtitlesSelectedIndex = int(input(">> Enter your choice (0-" + str(subtitlesItemIndex) + "): ")) | |
except KeyboardInterrupt: | |
sys.exit(1) | |
except: | |
subtitlesSelectedIndex = -1 | |
if subtitlesSelectedIndex == 0: | |
print("Cancelling search...") | |
return ("", -1) | |
subtitlesSelectedIndex = subtitlesSelectedIndex-1 | |
subtitlesSelectedName = subtitlesResultList['data'][subtitlesSelectedIndex]['attributes']['files'][0]['file_name'] | |
# Return the result (selected subtitles name and index) | |
return (subtitlesSelectedName, subtitlesSelectedIndex) | |
# ==== Automatic selection mode ================================================ | |
def selectionAuto(subtitlesResultList): | |
"""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, subtitle in enumerate(subtitlesResultList['data']): | |
score = 0 | |
# points to respect languages priority | |
score += languageListReversed.index(subtitle['SubLanguageID']) * 100 | |
# extra point if the sub is found by hash | |
if subtitle['MatchedBy'] == 'moviehash': | |
score += 1 | |
# points for filename mach | |
subFileParts = subtitle['SubFileName'].replace('-', '.').replace(' ', '.').replace('_', '.').lower().split('.') | |
for subPart in subFileParts: | |
for filePart in videoFileParts: | |
if subPart == filePart: | |
score += 1 | |
if score > maxScore: | |
maxScore = score | |
subtitlesSelectedName = subtitle['SubFileName'] | |
subtitlesSelectedIndex = idx | |
# Return the result (selected subtitles name and index) | |
return (subtitlesSelectedName, subtitlesSelectedIndex) | |
# ==== Check dependencies ====================================================== | |
def pythonChecker(): | |
"""Check the availability of python 3 interpreter""" | |
if sys.version_info < (3, 0): | |
superPrint("error", "Wrong Python version", "You need <b>Python 3</b> to use OpenSubtitlesDownload.") | |
return False | |
return True | |
def dependencyChecker(): | |
"""Check the availability of tools used as dependencies""" | |
if opt_gui != 'cli': | |
for tool in ['gunzip', 'wget']: | |
path = shutil.which(tool) | |
if path is None: | |
superPrint("error", "Missing dependency!", "The <b>'" + tool + "'</b> tool is not available, please install it!") | |
return False | |
return True | |
# ==== REST API helpers ======================================================== | |
def getUserToken(username, password): | |
try: | |
headers = { | |
"User-Agent": "OpenSubtitlesDownload v6.0", | |
"Api-key": f"{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_ENDPOINT, data=data, headers=headers) | |
with urllib.request.urlopen(req) as response: | |
response_data = json.loads(response.read().decode('utf-8')) | |
return response_data['token'] | |
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": "OpenSubtitlesDownload v6.0", | |
"Api-key": f"{API_KEY}" | |
} | |
query_params = urllib.parse.urlencode(kwargs) | |
url = f"{API_URL_SEARCH_ENDPOINT}?{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')) | |
return response_data | |
except Exception: | |
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0])) | |
def getSubtitlesInfo(file_id): | |
try: | |
USER_TOKEN = getUserToken(username=osd_username, password=osd_password) | |
headers = { | |
"User-Agent": "OpenSubtitlesDownload v6.0", | |
"Api-key": f"{API_KEY}", | |
"Accept": "application/json", | |
"Authorization": f"Bearer {USER_TOKEN}", | |
"Content-Type": "application/json" | |
} | |
payload = { | |
"file_id": file_id | |
} | |
data = json.dumps(payload).encode('utf-8') | |
req = urllib.request.Request(API_URL_DOWNLOAD_ENDPOINT, data=data, headers=headers) | |
with urllib.request.urlopen(req) as response: | |
result = response.read().decode('utf-8') | |
return json.loads(result) | |
except Exception: | |
print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0])) | |
# ============================================================================== | |
# ==== Main program (execution starts here) ==================================== | |
# ============================================================================== | |
# ==== Exit code returned by the software. You can use them to improve scripting behaviours. | |
# 0: Success, and subtitles downloaded | |
# 1: Success, but no subtitles found or downloaded | |
# 2: Failure | |
ExitCode = 2 | |
# ==== File and language lists | |
videoPathList = [] | |
languageList = [] | |
currentVideoPath = "" | |
currentLanguage = "" | |
# ==== Argument parsing | |
# Get OpenSubtitlesDownload.py script absolute path | |
if os.path.isabs(sys.argv[0]): | |
scriptPath = sys.argv[0] | |
else: | |
scriptPath = os.getcwd() + "/" + str(sys.argv[0]) | |
# Setup ArgumentParser | |
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('-l', '--lang', help="Specify the language in which the subtitles should be downloaded (default: en).\nSyntax:\n-l eng,fre: search in both language\n-l eng -l fre: download both language") | |
parser.add_argument('-i', '--skip', help="Skip search if an existing subtitles file is detected", action='store_true') | |
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('-o', '--output', help="Override subtitles download path, instead of next their video file") | |
parser.add_argument('-x', '--suffix', help="Force language code file suffix", action='store_true') | |
parser.add_argument('-8', '--utf8', help="Force UTF-8 file download", action='store_true') | |
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('searchPathList', help="The video file(s) or folder(s) for which subtitles should be searched and downloaded", nargs='+') | |
# Parse arguments | |
arguments = parser.parse_args() | |
# Handle arguments | |
if arguments.cli: | |
opt_gui = 'cli' | |
if arguments.gui: | |
opt_gui = arguments.gui | |
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.lang: | |
opt_languages = arguments.lang | |
if arguments.suffix: | |
opt_language_suffix = 'on' | |
if arguments.utf8: | |
opt_force_utf8 = True | |
if arguments.username and arguments.password: | |
osd_username = arguments.username | |
osd_password = arguments.password | |
# GUI auto detection | |
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 | |
# Sanitize some settings | |
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' | |
# Check for Python 3 | |
if pythonChecker() is False: | |
sys.exit(2) | |
# Check for the necessary tools (must be done after GUI auto detection) | |
if dependencyChecker() is False: | |
sys.exit(2) | |
# ==== Get video paths, validate them, and if needed check if subtitles already exists | |
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 videoPathList is empty, abort! | |
if not videoPathList: | |
sys.exit(1) | |
# ==== Instances dispatcher ==================================================== | |
# The first video file will be processed by this instance | |
currentVideoPath = videoPathList[0] | |
videoPathList.pop(0) | |
# The remaining file(s) are dispatched to new instance(s) of this script | |
for videoPathDispatch in videoPathList: | |
# Pass settings | |
command = [ sys.executable, scriptPath, "-g", opt_gui, "-s", opt_search_mode, "-t", opt_selection_mode ] | |
for language in opt_languages: | |
command.append("-l") | |
command.append(language) | |
if not opt_search_overwrite: | |
command.append("-i") | |
if opt_language_suffix == 'on': | |
command.append("-x") | |
if opt_force_utf8 == True: | |
command.append("-8") | |
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 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) | |
# ==== Search and download subtitles =========================================== | |
try: | |
# ==== Count languages selected for this search | |
if isinstance(opt_languages, list): | |
languageList = opt_languages | |
else: | |
languageList = opt_languages.split(',') | |
languageCount_search = len(languageList) | |
languageCount_results = 0 | |
# ==== Get file hash, size and name | |
videoTitle = u'' | |
videoHash = hashFile(currentVideoPath) | |
videoSize = os.path.getsize(currentVideoPath) | |
videoFileName = os.path.basename(currentVideoPath) | |
subtitlesResultList = '' | |
## Search for subtitles | |
try: | |
subtitlesResultList = searchSubtitles(moviehash=videoHash, languages=opt_languages) | |
#print(f"SEARCH BY HASH >>>>> length {len(subtitlesResultList['data'])} >>>>> {subtitlesResultList['data']}") | |
if len(subtitlesResultList['data']) > 0: | |
videoTitle = subtitlesResultList['data'][0]['attributes']['feature_details']['movie_name'] | |
# if we didn't get any results for hash try searching by name | |
if 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!\n<b>Search error</b>") | |
sys.exit(2) | |
## Parse the results of the search query | |
if ('data' in subtitlesResultList) and (subtitlesResultList['data']): | |
# 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']['moviehash_match'] == True): | |
if opt_selection_mode != 'manual': | |
subName = subtitlesResultList['data'][0]['attributes']['files'][0]['file_id'] | |
# Title and filename may need string sanitizing to avoid zenity/kdialog handling errors | |
if opt_gui != 'cli': | |
videoTitle = videoTitle.replace('"', '\\"') | |
videoTitle = videoTitle.replace("'", "\\'") | |
videoTitle = videoTitle.replace('`', '\\`') | |
videoTitle = videoTitle.replace("&", "&") | |
videoFileName = videoFileName.replace('"', '\\"') | |
videoFileName = videoFileName.replace("'", "\\'") | |
videoFileName = videoFileName.replace('`', '\\`') | |
videoFileName = videoFileName.replace("&", "&") | |
# 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) | |
else: | |
# Go through the list of subtitles and handle 'auto' settings activation | |
for item in subtitlesResultList['data']: | |
if opt_selection_match == 'auto' and opt_search_mode == 'hash_and_filename': | |
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']['hearing_impaired'] == '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' | |
# 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: | |
# Prepare download | |
fileId = subtitlesResultList['data'][int(subIndex)]['attributes']['files'][0]['file_id'] | |
fileInfo = getSubtitlesInfo(fileId) | |
# quote the URL to avoid characters like brackets () causing errors in wget command below | |
subURL = f"\'{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 | |
# Escape non-alphanumeric characters from the subtitles download path | |
if opt_gui != 'cli': | |
subPath = re.escape(subPath) | |
subPath = subPath.replace('"', '\\"') | |
subPath = subPath.replace("'", "\\'") | |
subPath = subPath.replace('`', '\\`') | |
## Download and unzip the selected subtitles | |
if opt_gui == 'gnome': | |
process_subtitlesDownload = subprocess.call("(wget -q -O " + subPath + " " + subURL + ") 2>&1" | |
+ ' | (zenity --auto-close --progress --pulsate --title="Downloading subtitles, please wait..." --text="Downloading <b>' | |
+ subLangName + '</b> subtitles for <b>' + videoTitle + '</b>...")', shell=True) | |
elif opt_gui == 'kde': | |
process_subtitlesDownload = subprocess.call("(wget -q -O " + subPath + " " + subURL + ") 2>&1", shell=True) | |
else: # CLI | |
print(">> Downloading '" + subtitlesResultList['data'][subIndex]['attributes']['language'] + "' subtitles for '" + videoTitle + "'") | |
downloadResult = subprocess.run("(wget -q -O " + subPath + " " + subURL + ") 2>&1", shell=True) | |
process_subtitlesDownload = downloadResult.returncode | |
# If an error occurs, say so | |
if process_subtitlesDownload != 0: | |
superPrint("error", "Subtitling error!", | |
"An error occurred while downloading or writing <b>" + subtitlesResultList['data'][subIndex]['attributes']['language'] + "</b> subtitles for <b>" + videoTitle + "</b>.") | |
sys.exit(2) | |
## HOOK # Use a secondary tool after a successful download? | |
#process_subtitlesDownload = subprocess.call("(custom_command" + " " + subPath + ") 2>&1", 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 :-(", '<b>No subtitles found</b> for this video:\n<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 <b>unknown error</b>, sorry about that...\n\n" + \ | |
"Error: <b>" + str(sys.exc_info()[0]).replace('<', '[').replace('>', ']') + "</b>\n" + \ | |
"Line: <b>" + str(sys.exc_info()[-1].tb_lineno) + "</b>\n\n" + \ | |
"Just to be safe, please check:\n" + \ | |
"- Your Internet connection status\n" + \ | |
"- www.opensubtitles.com availability\n" + \ | |
"- Your download limits (5 subtitles per 24h for free (non VIP) users, 5 subtitles per 10s)\n" + \ | |
"- 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])) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment