bins

handy scripts
git clone git://popovic.xyz/bins.git
Log | Files | Refs | LICENSE

fsubs (39468B)


      1 #!/usr/bin/env python
      2 # -*- coding: utf-8 -*-
      3 
      4 # OpenSubtitlesDownload.py / Version 4.2
      5 # This software is designed to help you find and download subtitles for your favorite videos!
      6 
      7 # You can browse the project's GitHub page:
      8 # - https://github.com/emericg/OpenSubtitlesDownload
      9 # Learn much more about configuring OpenSubtitlesDownload.py on its wiki:
     10 # - https://github.com/emericg/OpenSubtitlesDownload/wiki
     11 
     12 # Copyright (c) 2020 by Emeric GRANGE <emeric.grange@gmail.com>
     13 #
     14 # This program is free software: you can redistribute it and/or modify
     15 # it under the terms of the GNU General Public License as published by
     16 # the Free Software Foundation, either version 3 of the License, or
     17 # (at your option) any later version.
     18 #
     19 # This program is distributed in the hope that it will be useful,
     20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     22 # GNU General Public License for more details.
     23 #
     24 # You should have received a copy of the GNU General Public License
     25 # along with this program.  If not, see <https://www.gnu.org/licenses/>.
     26 
     27 # Contributors / special thanks:
     28 # Thiago Alvarenga Lechuga <thiagoalz@gmail.com> for his work on the 'Windows CLI' and the 'folder search'
     29 # jeroenvdw for his work on the 'subtitles automatic selection' and the 'search by filename'
     30 # Gui13 for his work on the arguments parsing
     31 # Tomáš Hnyk <tomashnyk@gmail.com> for his work on the 'multiple language' feature
     32 # Carlos Acedo <carlos@linux-labs.net> for his work on the original script
     33 
     34 import os
     35 import re
     36 import sys
     37 import time
     38 import gzip
     39 import struct
     40 import argparse
     41 import mimetypes
     42 import subprocess
     43 
     44 if sys.version_info >= (3, 0):
     45     import shutil
     46     import urllib.request
     47     from xmlrpc.client import ServerProxy, Error
     48 else: # python2
     49     import urllib
     50     from xmlrpclib import ServerProxy, Error
     51 
     52 # ==== Opensubtitles.org server settings =======================================
     53 
     54 # XML-RPC server domain for opensubtitles.org:
     55 osd_server = ServerProxy('https://api.opensubtitles.org/xml-rpc')
     56 
     57 # You can use your opensubtitles.org VIP account to avoid "in-subtitles" advertisement and bypass download limits.
     58 # Be careful about your password security, it will be stored right here in plain text...
     59 # You can also change opensubtitles.org language, it will be used for error codes and stuff.
     60 # Can be overridden at run time with '-u' and '-p' arguments.
     61 osd_username = 'miksa234'
     62 osd_password = 'kaxedor'
     63 osd_language = 'en'
     64 
     65 # ==== Language settings =======================================================
     66 
     67 # 1/ Change the search language by using any supported 3 letter language codes:
     68 #    > Supported language codes: https://www.opensubtitles.org/addons/export_languages.php
     69 # 2/ Search for subtitles in several languages at once by using multiple codes separated by a comma:
     70 #    > Ex: opt_languages = ['eng,fre']
     71 opt_languages = ['eng']
     72 
     73 # Write 2-letter language code (ex: _en) at the end of the subtitles file. 'on', 'off' or 'auto'.
     74 # If you are regularly searching for several language at once, you sould use 'on'.
     75 # The language code used will be the one provided in opt_languages.
     76 # Can be overridden at run time with '-x' arguments.
     77 opt_language_suffix = 'auto'
     78 opt_language_separator = '_'
     79 
     80 # Force downloading and storing UTF-8 encoded subtitles files.
     81 opt_force_utf8 = True
     82 
     83 # ==== Search settings =========================================================
     84 
     85 # Subtitles search mode. Can be overridden at run time with '-s' argument.
     86 # - hash (search by hash only)
     87 # - filename (search by filename only)
     88 # - hash_then_filename (search by hash, then if no results by filename)
     89 # - hash_and_filename (search using both methods)
     90 opt_search_mode = 'hash_then_filename'
     91 
     92 # Search and download a subtitles even if a subtitles file already exists.
     93 opt_search_overwrite = True
     94 
     95 # Subtitles selection mode. Can be overridden at run time with '-t' argument.
     96 # - manual (always let you choose the subtitles you want)
     97 # - default (in case of multiple results, let you choose the subtitles you want)
     98 # - auto (automatically select the best subtitles found)
     99 opt_selection_mode = 'default'
    100 
    101 # Customize subtitles download path. Can be overridden at run time with '-o' argument.
    102 # By default, subtitles are downloaded next to their video file.
    103 opt_output_path = ''
    104 
    105 # ==== GUI settings ============================================================
    106 
    107 # Select your GUI. Can be overridden at run time with '--gui=xxx' argument.
    108 # - auto (autodetection, fallback on CLI)
    109 # - gnome (GNOME/GTK based environments, using 'zenity' backend)
    110 # - kde (KDE/Qt based environments, using 'kdialog' backend)
    111 # - cli (Command Line Interface)
    112 opt_gui = 'auto'
    113 
    114 # Change the subtitles selection GUI size:
    115 opt_gui_width  = 720
    116 opt_gui_height = 320
    117 
    118 # Various GUI options. You can set them to 'on', 'off' or 'auto'.
    119 opt_selection_hi       = 'auto'
    120 opt_selection_language = 'auto'
    121 opt_selection_match    = 'auto'
    122 opt_selection_rating   = 'off'
    123 opt_selection_count    = 'off'
    124 
    125 # ==== Exit codes ==============================================================
    126 
    127 # Exit code returned by the software. You can use them to improve scripting behaviours.
    128 # 0: Success, and subtitles downloaded
    129 # 1: Success, but no subtitles found
    130 # 2: Failure
    131 
    132 # ==== Super Print =============================================================
    133 # priority: info, warning, error
    134 # title: only for zenity and kdialog messages
    135 # message: full text, with tags and breaks (tags will be cleaned up for CLI)
    136 
    137 def superPrint(priority, title, message):
    138     """Print messages through terminal, zenity or kdialog"""
    139     if opt_gui == 'gnome':
    140         subprocess.call(['zenity', '--width=' + str(opt_gui_width), '--' + priority, '--title=' + title, '--text=' + message])
    141     elif opt_gui == 'kde':
    142         # Adapt to kdialog
    143         message = message.replace("\n", "<br>")
    144         message = message.replace('\\"', '"')
    145         if priority == 'warning':
    146             priority = 'sorry'
    147         elif priority == 'info':
    148             priority = 'msgbox'
    149 
    150         subprocess.call(['kdialog', '--geometry=' + str(opt_gui_width) + 'x' + str(opt_gui_height), '--title=' + title, '--' + priority + '=' + message])
    151     else:
    152         # Clean up formating tags from the zenity messages
    153         message = message.replace("\n\n", "\n")
    154         message = message.replace('\\"', '"')
    155         message = message.replace("<i>", "")
    156         message = message.replace("</i>", "")
    157         message = message.replace("<b>", "")
    158         message = message.replace("</b>", "")
    159 
    160         print(">> " + message)
    161 
    162 # ==== Check file path & type ==================================================
    163 
    164 def checkFileValidity(path):
    165     """Check mimetype and/or file extension to detect valid video file"""
    166     if os.path.isfile(path) is False:
    167         return False
    168 
    169     fileMimeType, encoding = mimetypes.guess_type(path)
    170     if fileMimeType is None:
    171         fileExtension = path.rsplit('.', 1)
    172         if fileExtension[1] not in ['avi', 'mp4', 'mov', 'mkv', 'mk3d', 'webm', \
    173                                     'ts', 'mts', 'm2ts', 'ps', 'vob', 'evo', 'mpeg', 'mpg', \
    174                                     'm1v', 'm2p', 'm2v', 'm4v', 'movhd', 'movx', 'qt', \
    175                                     'mxf', 'ogg', 'ogm', 'ogv', 'rm', 'rmvb', 'flv', 'swf', \
    176                                     'asf', 'wm', 'wmv', 'wmx', 'divx', 'x264', 'xvid']:
    177             #superPrint("error", "File type error!", "This file is not a video (unknown mimetype AND invalid file extension):\n<i>" + path + "</i>")
    178             return False
    179     else:
    180         fileMimeType = fileMimeType.split('/', 1)
    181         if fileMimeType[0] != 'video':
    182             #superPrint("error", "File type error!", "This file is not a video (unknown mimetype):\n<i>" + path + "</i>")
    183             return False
    184 
    185     return True
    186 
    187 # ==== Check for existing subtitles file =======================================
    188 
    189 def checkSubtitlesExists(path):
    190     """Check if a subtitles already exists for the current file"""
    191     for ext in ['srt', 'sub', 'sbv', 'smi', 'ssa', 'ass', 'usf']:
    192         subPath = path.rsplit('.', 1)[0] + '.' + ext
    193         if os.path.isfile(subPath) is True:
    194             superPrint("info", "Subtitles already downloaded!", "A subtitles file already exists for this file:\n<i>" + subPath + "</i>")
    195             return True
    196         # With language code?
    197         if opt_language_suffix in ('on', 'auto'):
    198             if len(opt_languages) == 1:
    199                 splitted_languages_list = opt_languages[0].split(',')
    200             else:
    201                 splitted_languages_list = opt_languages.split
    202             for language in splitted_languages_list:
    203                 # Rough method to try 2 and 3 letters language codes
    204                 if len(language) == 3:
    205                     splitted_languages_list.append(language[0:2])
    206             for language in splitted_languages_list:
    207                 subPath = path.rsplit('.', 1)[0] + opt_language_separator + language + '.' + ext
    208                 if os.path.isfile(subPath) is True:
    209                     superPrint("info", "Subtitles already downloaded!", "A subtitles file already exists for this file:\n<i>" + subPath + "</i>")
    210                     return True
    211 
    212     return False
    213 
    214 # ==== Hashing algorithm =======================================================
    215 # Info: https://trac.opensubtitles.org/projects/opensubtitles/wiki/HashSourceCodes
    216 # This particular implementation is coming from SubDownloader: https://subdownloader.net
    217 
    218 def hashFile(path):
    219     """Produce a hash for a video file: size + 64bit chksum of the first and
    220     last 64k (even if they overlap because the file is smaller than 128k)"""
    221     try:
    222         longlongformat = 'Q' # unsigned long long little endian
    223         bytesize = struct.calcsize(longlongformat)
    224         fmt = "<%d%s" % (65536//bytesize, longlongformat)
    225 
    226         f = open(path, "rb")
    227 
    228         filesize = os.fstat(f.fileno()).st_size
    229         filehash = filesize
    230 
    231         if filesize < 65536 * 2:
    232             superPrint("error", "File size error!", "File size error while generating hash for this file:\n<i>" + path + "</i>")
    233             return "SizeError"
    234 
    235         buf = f.read(65536)
    236         longlongs = struct.unpack(fmt, buf)
    237         filehash += sum(longlongs)
    238 
    239         f.seek(-65536, os.SEEK_END) # size is always > 131072
    240         buf = f.read(65536)
    241         longlongs = struct.unpack(fmt, buf)
    242         filehash += sum(longlongs)
    243         filehash &= 0xFFFFFFFFFFFFFFFF
    244 
    245         f.close()
    246         returnedhash = "%016x" % filehash
    247         return returnedhash
    248 
    249     except IOError:
    250         superPrint("error", "I/O error!", "Input/Output error while generating hash for this file:\n<i>" + path + "</i>")
    251         return "IOError"
    252 
    253 # ==== GNOME selection window ==================================================
    254 
    255 def selectionGnome(subtitlesList):
    256     """GNOME subtitles selection window using zenity"""
    257     subtitlesSelected = ''
    258     subtitlesItems = ''
    259     subtitlesMatchedByHash = 0
    260     subtitlesMatchedByName = 0
    261     columnHi = ''
    262     columnLn = ''
    263     columnMatch = ''
    264     columnRate = ''
    265     columnCount = ''
    266 
    267     # Generate selection window content
    268     for item in subtitlesList['data']:
    269         if item['MatchedBy'] == 'moviehash':
    270             subtitlesMatchedByHash += 1
    271         else:
    272             subtitlesMatchedByName += 1
    273 
    274         subtitlesItems += '"' + item['SubFileName'] + '" '
    275 
    276         if opt_selection_hi == 'on':
    277             columnHi = '--column="HI" '
    278             if item['SubHearingImpaired'] == '1':
    279                 subtitlesItems += '"✔" '
    280             else:
    281                 subtitlesItems += '"" '
    282         if opt_selection_language == 'on':
    283             columnLn = '--column="Language" '
    284             subtitlesItems += '"' + item['LanguageName'] + '" '
    285         if opt_selection_match == 'on':
    286             columnMatch = '--column="MatchedBy" '
    287             if item['MatchedBy'] == 'moviehash':
    288                 subtitlesItems += '"HASH" '
    289             else:
    290                 subtitlesItems += '"" '
    291         if opt_selection_rating == 'on':
    292             columnRate = '--column="Rating" '
    293             subtitlesItems += '"' + item['SubRating'] + '" '
    294         if opt_selection_count == 'on':
    295             columnCount = '--column="Downloads" '
    296             subtitlesItems += '"' + item['SubDownloadsCnt'] + '" '
    297 
    298     if subtitlesMatchedByName == 0:
    299         tilestr = ' --title="Subtitles for: ' + videoTitle + '"'
    300         textstr = ' --text="<b>Video title:</b> ' + videoTitle + '\n<b>File name:</b> ' + videoFileName + '"'
    301     elif subtitlesMatchedByHash == 0:
    302         tilestr = ' --title="Subtitles for: ' + videoFileName + '"'
    303         textstr = ' --text="Search results using file name, NOT video detection. <b>May be unreliable...</b>\n<b>File name:</b> ' + videoFileName + '" '
    304     else: # a mix of the two
    305         tilestr = ' --title="Subtitles for: ' + videoTitle + '"'
    306         textstr = ' --text="Search results using file name AND video detection.\n<b>Video title:</b> ' + videoTitle + '\n<b>File name:</b> ' + videoFileName + '"'
    307 
    308     # Spawn zenity "list" dialog
    309     process_subtitlesSelection = subprocess.Popen('zenity --width=' + str(opt_gui_width) + ' --height=' + str(opt_gui_height) + ' --list' + tilestr + textstr \
    310         + ' --column="Available subtitles" ' + columnHi + columnLn + columnMatch + columnRate + columnCount + subtitlesItems, shell=True, stdout=subprocess.PIPE)
    311 
    312     # Get back the result
    313     result_subtitlesSelection = process_subtitlesSelection.communicate()
    314 
    315     # The results contain a subtitles?
    316     if result_subtitlesSelection[0]:
    317         if sys.version_info >= (3, 0):
    318             subtitlesSelected = str(result_subtitlesSelection[0], 'utf-8').strip("\n")
    319         else: # python2
    320             subtitlesSelected = str(result_subtitlesSelection[0]).strip("\n")
    321 
    322         # Hack against recent zenity version?
    323         if len(subtitlesSelected.split("|")) > 1:
    324             if subtitlesSelected.split("|")[0] == subtitlesSelected.split("|")[1]:
    325                 subtitlesSelected = subtitlesSelected.split("|")[0]
    326     else:
    327         if process_subtitlesSelection.returncode == 0:
    328             subtitlesSelected = subtitlesList['data'][0]['SubFileName']
    329 
    330     # Return the result
    331     return subtitlesSelected
    332 
    333 # ==== KDE selection window ====================================================
    334 
    335 def selectionKde(subtitlesList):
    336     """KDE subtitles selection window using kdialog"""
    337     subtitlesSelected = ''
    338     subtitlesItems = ''
    339     subtitlesMatchedByHash = 0
    340     subtitlesMatchedByName = 0
    341 
    342     # Generate selection window content
    343     # TODO doesn't support additional columns
    344     index = 0
    345     for item in subtitlesList['data']:
    346         if item['MatchedBy'] == 'moviehash':
    347             subtitlesMatchedByHash += 1
    348         else:
    349             subtitlesMatchedByName += 1
    350 
    351         # key + subtitles name
    352         subtitlesItems += str(index) + ' "' + item['SubFileName'] + '" '
    353         index += 1
    354 
    355     if subtitlesMatchedByName == 0:
    356         tilestr = ' --title="Subtitles for ' + videoTitle + '"'
    357         menustr = ' --menu="<b>Video title:</b> ' + videoTitle + '<br><b>File name:</b> ' + videoFileName + '" '
    358     elif subtitlesMatchedByHash == 0:
    359         tilestr = ' --title="Subtitles for ' + videoFileName + '"'
    360         menustr = ' --menu="Search results using file name, NOT video detection. <b>May be unreliable...</b><br><b>File name:</b> ' + videoFileName + '" '
    361     else: # a mix of the two
    362         tilestr = ' --title="Subtitles for ' + videoTitle + '" '
    363         menustr = ' --menu="Search results using file name AND video detection.<br><b>Video title:</b> ' + videoTitle + '<br><b>File name:</b> ' + videoFileName + '" '
    364 
    365     # Spawn kdialog "radiolist"
    366     process_subtitlesSelection = subprocess.Popen('kdialog --geometry=' + str(opt_gui_width) + 'x' + str(opt_gui_height) + tilestr + menustr + subtitlesItems, shell=True, stdout=subprocess.PIPE)
    367 
    368     # Get back the result
    369     result_subtitlesSelection = process_subtitlesSelection.communicate()
    370 
    371     # The results contain the key matching a subtitles?
    372     if result_subtitlesSelection[0]:
    373         if sys.version_info >= (3, 0):
    374             keySelected = int(str(result_subtitlesSelection[0], 'utf-8').strip("\n"))
    375         else: # python2
    376             keySelected = int(str(result_subtitlesSelection[0]).strip("\n"))
    377 
    378         subtitlesSelected = subtitlesList['data'][keySelected]['SubFileName']
    379 
    380     # Return the result
    381     return subtitlesSelected
    382 
    383 # ==== CLI selection mode ======================================================
    384 
    385 def selectionCLI(subtitlesList):
    386     """Command Line Interface, subtitles selection inside your current terminal"""
    387     subtitlesIndex = 0
    388     subtitlesItem = ''
    389 
    390     # Print video infos
    391     print("\n>> Title: " + videoTitle)
    392     print(">> Filename: " + videoFileName)
    393 
    394     # Print subtitles list on the terminal
    395     print(">> Available subtitles:")
    396     for item in subtitlesList['data']:
    397         subtitlesIndex += 1
    398         subtitlesItem = '"' + item['SubFileName'] + '" '
    399 
    400         if opt_selection_hi == 'on' and item['SubHearingImpaired'] == '1':
    401             subtitlesItem += '> "HI" '
    402         if opt_selection_language == 'on':
    403             subtitlesItem += '> "Language: ' + item['LanguageName'] + '" '
    404         if opt_selection_match == 'on':
    405             subtitlesItem += '> "MatchedBy: ' + item['MatchedBy'] + '" '
    406         if opt_selection_rating == 'on':
    407             subtitlesItem += '> "SubRating: ' + item['SubRating'] + '" '
    408         if opt_selection_count == 'on':
    409             subtitlesItem += '> "SubDownloadsCnt: ' + item['SubDownloadsCnt'] + '" '
    410 
    411         if item['MatchedBy'] == 'moviehash':
    412             print("\033[92m[" + str(subtitlesIndex) + "]\033[0m " + subtitlesItem)
    413         else:
    414             print("\033[93m[" + str(subtitlesIndex) + "]\033[0m " + subtitlesItem)
    415 
    416     # Ask user selection
    417     print("\033[91m[0]\033[0m Cancel search")
    418     sub_selection = -1
    419     while(sub_selection < 0 or sub_selection > subtitlesIndex):
    420         try:
    421             if sys.version_info >= (3, 0):
    422                 sub_selection = int(input(">> Enter your choice (0-" + str(subtitlesIndex) + "): "))
    423             else: # python 2
    424                 sub_selection = int(raw_input(">> Enter your choice (0-" + str(subtitlesIndex) + "): "))
    425         except KeyboardInterrupt:
    426             sys.exit(0)
    427         except:
    428             sub_selection = -1
    429 
    430     # Return the result
    431     if sub_selection == 0:
    432         print("Cancelling search...")
    433         return ""
    434 
    435     return subtitlesList['data'][sub_selection-1]['SubFileName']
    436 
    437 # ==== Automatic selection mode ================================================
    438 
    439 def selectionAuto(subtitlesList):
    440     """Automatic subtitles selection using filename match"""
    441 
    442     if len(opt_languages) == 1:
    443         splitted_languages_list = list(reversed(opt_languages[0].split(',')))
    444     else:
    445         splitted_languages_list = opt_languages
    446 
    447     videoFileParts = videoFileName.replace('-', '.').replace(' ', '.').replace('_', '.').lower().split('.')
    448     maxScore = -1
    449 
    450     for subtitle in subtitlesList['data']:
    451         score = 0
    452         # points to respect languages priority
    453         score += splitted_languages_list.index(subtitle['SubLanguageID']) * 100
    454         # extra point if the sub is found by hash
    455         if subtitle['MatchedBy'] == 'moviehash':
    456             score += 1
    457         # points for filename mach
    458         subFileParts = subtitle['SubFileName'].replace('-', '.').replace(' ', '.').replace('_', '.').lower().split('.')
    459         for subPart in subFileParts:
    460             for filePart in videoFileParts:
    461                 if subPart == filePart:
    462                     score += 1
    463         if score > maxScore:
    464             maxScore = score
    465             subtitlesSelected = subtitle['SubFileName']
    466 
    467     return subtitlesSelected
    468 
    469 # ==== Check dependencies ======================================================
    470 
    471 def dependencyChecker():
    472     """Check the availability of tools used as dependencies"""
    473 
    474     if opt_gui != 'cli':
    475         if sys.version_info >= (3, 3):
    476             for tool in ['gunzip', 'wget']:
    477                 path = shutil.which(tool)
    478                 if path is None:
    479                     superPrint("error", "Missing dependency!", "The <b>'" + tool + "'</b> tool is not available, please install it!")
    480                     return False
    481 
    482     return True
    483 
    484 # ==============================================================================
    485 # ==== Main program (execution starts here) ====================================
    486 # ==============================================================================
    487 
    488 ExitCode = 2
    489 
    490 # ==== Argument parsing
    491 
    492 # Get OpenSubtitlesDownload.py script absolute path
    493 if os.path.isabs(sys.argv[0]):
    494     scriptPath = sys.argv[0]
    495 else:
    496     scriptPath = os.getcwd() + "/" + str(sys.argv[0])
    497 
    498 # Setup ArgumentParser
    499 parser = argparse.ArgumentParser(prog='OpenSubtitlesDownload.py',
    500                                  description='Automatically find and download the right subtitles for your favorite videos!',
    501                                  formatter_class=argparse.RawTextHelpFormatter)
    502 
    503 parser.add_argument('--cli', help="Force CLI mode", action='store_true')
    504 parser.add_argument('-g', '--gui', help="Select the GUI you want from: auto, kde, gnome, cli (default: auto)")
    505 parser.add_argument('-l', '--lang', help="Specify the language in which the subtitles should be downloaded (default: eng).\nSyntax:\n-l eng,fre: search in both language\n-l eng -l fre: download both language", nargs='?', action='append')
    506 parser.add_argument('-i', '--skip', help="Skip search if an existing subtitles file is detected", action='store_true')
    507 parser.add_argument('-s', '--search', help="Search mode: hash, filename, hash_then_filename, hash_and_filename (default: hash_then_filename)")
    508 parser.add_argument('-t', '--select', help="Selection mode: manual, default, auto")
    509 parser.add_argument('-a', '--auto', help="Force automatic selection and download of the best subtitles found", action='store_true')
    510 parser.add_argument('-o', '--output', help="Override subtitles download path, instead of next their video file")
    511 parser.add_argument('-x', '--suffix', help="Enable language code suffix", action='store_true')
    512 parser.add_argument('-u', '--username', help="Set opensubtitles.org account username")
    513 parser.add_argument('-p', '--password', help="Set opensubtitles.org account password")
    514 
    515 parser.add_argument('filePathListArg', help="The video file(s) for which subtitles should be searched and downloaded", nargs='+')
    516 
    517 # Only use ArgumentParser if we have arguments...
    518 if len(sys.argv) > 1:
    519     result = parser.parse_args()
    520 
    521     # Handle results
    522     if result.cli:
    523         opt_gui = 'cli'
    524     if result.gui:
    525         opt_gui = result.gui
    526     if result.search:
    527         opt_search_mode = result.search
    528     if result.skip:
    529         opt_search_overwrite = False
    530     if result.select:
    531         opt_selection_mode = result.select
    532     if result.auto:
    533         opt_selection_mode = 'auto'
    534     if result.output:
    535         opt_output_path = result.output
    536     if result.lang:
    537         if opt_languages != result.lang:
    538             opt_languages = result.lang
    539             opt_selection_language = 'on'
    540     if result.suffix:
    541         opt_language_suffix = 'on'
    542     if result.username and result.password:
    543         osd_username = result.username
    544         osd_password = result.password
    545 
    546 # GUI auto detection
    547 if opt_gui == 'auto':
    548     # Note: "ps cax" only output the first 15 characters of the executable's names
    549     ps = str(subprocess.Popen(['ps', 'cax'], stdout=subprocess.PIPE).communicate()[0]).split('\n')
    550     for line in ps:
    551         if ('gnome-session' in line) or ('cinnamon-sessio' in line) or ('mate-session' in line) or ('xfce4-session' in line):
    552             opt_gui = 'gnome'
    553             break
    554         elif 'ksmserver' in line:
    555             opt_gui = 'kde'
    556             break
    557 
    558 # Sanitize settings
    559 if opt_search_mode not in ['hash', 'filename', 'hash_then_filename', 'hash_and_filename']:
    560     opt_search_mode = 'hash_then_filename'
    561 
    562 if opt_selection_mode not in ['manual', 'default', 'auto']:
    563     opt_selection_mode = 'default'
    564 
    565 if opt_gui not in ['gnome', 'kde', 'cli']:
    566     opt_gui = 'cli'
    567     opt_search_mode = 'hash_then_filename'
    568     opt_selection_mode = 'auto'
    569     print("Unknown GUI, falling back to an automatic CLI mode")
    570 
    571 # ==== Check for the necessary tools (must be done after GUI auto detection)
    572 
    573 if dependencyChecker() is False:
    574     sys.exit(2)
    575 
    576 # ==== Get valid video paths
    577 
    578 videoPathList = []
    579 
    580 if 'result' in locals():
    581     # Go through the paths taken from arguments, and extract only valid video paths
    582     for i in result.filePathListArg:
    583         filePath = os.path.abspath(i)
    584         if os.path.isdir(filePath):
    585             if opt_gui == 'cli':
    586                 # If it is a folder, check all of its files recursively
    587                 for root, _, items in os.walk(filePath):
    588                     for item in items:
    589                         localPath = os.path.join(root, item)
    590                         if checkFileValidity(localPath):
    591                             videoPathList.append(localPath)
    592             else:
    593                 # If it is a folder, check all of its files
    594                 for item in os.listdir(filePath):
    595                     localPath = os.path.join(filePath, item)
    596                     if checkFileValidity(localPath):
    597                         videoPathList.append(localPath)
    598         elif checkFileValidity(filePath):
    599             # If it is a valid file, use it
    600             videoPathList.append(filePath)
    601 else:
    602     superPrint("error", "No file provided!", "No file provided!")
    603     sys.exit(2)
    604 
    605 # If videoPathList is empty, abort!
    606 if not videoPathList:
    607     parser.print_help()
    608     sys.exit(1)
    609 
    610 # Check if the subtitles files already exists
    611 if not opt_search_overwrite:
    612     videoPathList = [path for path in videoPathList if not checkSubtitlesExists(path)]
    613 
    614     # If videoPathList is empty, exit!
    615     if not videoPathList:
    616         sys.exit(1)
    617 
    618 # ==== Instances dispatcher ====================================================
    619 
    620 # The first video file will be processed by this instance
    621 videoPath = videoPathList[0]
    622 videoPathList.pop(0)
    623 
    624 # The remaining file(s) are dispatched to new instance(s) of this script
    625 for videoPathDispatch in videoPathList:
    626 
    627     # Handle current options
    628     command = [ sys.executable, scriptPath, "-g", opt_gui, "-s", opt_search_mode, "-t", opt_selection_mode ]
    629 
    630     for resultlangs in opt_languages:
    631         command.append("-l")
    632         command.append(resultlangs)
    633 
    634     if not opt_search_overwrite:
    635         command.append("--skip")
    636 
    637     command.append(videoPathDispatch)
    638 
    639     # Do not spawn too many instances at once
    640     time.sleep(1)
    641 
    642     if opt_gui == 'cli' and opt_selection_mode != 'auto':
    643         # Synchronous call
    644         process_videoDispatched = subprocess.call(command)
    645     else:
    646         # Asynchronous call
    647         process_videoDispatched = subprocess.Popen(command)
    648 
    649 # ==== Search and download subtitles ===========================================
    650 
    651 try:
    652     # ==== Connection to OpenSubtitlesDownload
    653     try:
    654         session = osd_server.LogIn(osd_username, osd_password, osd_language, 'opensubtitles-download 4.2')
    655     except Exception:
    656         # Retry once after a delay (could just be a momentary overloaded server?)
    657         time.sleep(3)
    658         try:
    659             session = osd_server.LogIn(osd_username, osd_password, osd_language, 'opensubtitles-download 4.2')
    660         except Exception:
    661             superPrint("error", "Connection error!", "Unable to reach opensubtitles.org servers!\n\nPlease check:\n" + \
    662                 "- Your Internet connection status\n- www.opensubtitles.org availability\n- Your downloads limit (200 subtitles per 24h)\n\n" + \
    663                 "The subtitles search and download service is powered by opensubtitles.org. Be sure to donate if you appreciate the service provided!")
    664             sys.exit(2)
    665 
    666     # Connection refused?
    667     if session['status'] != '200 OK':
    668         superPrint("error", "Connection error!", "Opensubtitles.org servers refused the connection: " + session['status'] + ".\n\nPlease check:\n" + \
    669             "- Your Internet connection status\n - www.opensubtitles.org availability\n - Your downloads limit (200 subtitles per 24h)\n\n" + \
    670             "The subtitles search and download service is powered by opensubtitles.org. Be sure to donate if you appreciate the service provided!")
    671         sys.exit(2)
    672 
    673     # Count languages marked for this search
    674     searchLanguage = 0
    675     searchLanguageResult = 0
    676     for SubLanguageID in opt_languages:
    677         searchLanguage += len(SubLanguageID.split(','))
    678 
    679     searchResultPerLanguage = [searchLanguage]
    680 
    681     # ==== Get file hash, size and name
    682     videoTitle = ''
    683     videoHash = hashFile(videoPath)
    684     videoSize = os.path.getsize(videoPath)
    685     videoFileName = os.path.basename(videoPath)
    686 
    687     # ==== Search for available subtitles on OpenSubtitlesDownload
    688     for SubLanguageID in opt_languages:
    689         searchList = []
    690         subtitlesList = {}
    691 
    692         if opt_search_mode in ('hash', 'hash_then_filename', 'hash_and_filename'):
    693             searchList.append({'sublanguageid':SubLanguageID, 'moviehash':videoHash, 'moviebytesize':str(videoSize)})
    694         if opt_search_mode in ('filename', 'hash_and_filename'):
    695             searchList.append({'sublanguageid':SubLanguageID, 'query':videoFileName})
    696 
    697         ## Primary search
    698         try:
    699             subtitlesList = osd_server.SearchSubtitles(session['token'], searchList)
    700         except Exception:
    701             # Retry once after a delay (we are already connected, the server may be momentary overloaded)
    702             time.sleep(3)
    703             try:
    704                 subtitlesList = osd_server.SearchSubtitles(session['token'], searchList)
    705             except Exception:
    706                 superPrint("error", "Search error!", "Unable to reach opensubtitles.org servers!\n<b>Search error</b>")
    707 
    708         #if (opt_search_mode == 'hash_and_filename'):
    709         #    TODO Cleanup duplicate between moviehash and filename results
    710 
    711         ## Fallback search
    712         if ((opt_search_mode == 'hash_then_filename') and (('data' in subtitlesList) and (not subtitlesList['data']))):
    713             searchList[:] = [] # searchList.clear()
    714             searchList.append({'sublanguageid':SubLanguageID, 'query':videoFileName})
    715             subtitlesList.clear()
    716             try:
    717                 subtitlesList = osd_server.SearchSubtitles(session['token'], searchList)
    718             except Exception:
    719                 # Retry once after a delay (we are already connected, the server may be momentary overloaded)
    720                 time.sleep(3)
    721                 try:
    722                     subtitlesList = osd_server.SearchSubtitles(session['token'], searchList)
    723                 except Exception:
    724                     superPrint("error", "Search error!", "Unable to reach opensubtitles.org servers!\n<b>Search error</b>")
    725 
    726         ## Parse the results of the XML-RPC query
    727         if ('data' in subtitlesList) and (subtitlesList['data']):
    728 
    729             # Mark search as successful
    730             searchLanguageResult += 1
    731             subtitlesSelected = ''
    732 
    733             # If there is only one subtitles (matched by file hash), auto-select it (except in CLI mode)
    734             if (len(subtitlesList['data']) == 1) and (subtitlesList['data'][0]['MatchedBy'] == 'moviehash'):
    735                 if opt_selection_mode != 'manual':
    736                     subtitlesSelected = subtitlesList['data'][0]['SubFileName']
    737 
    738             # Get video title
    739             videoTitle = subtitlesList['data'][0]['MovieName']
    740 
    741             # Title and filename may need string sanitizing to avoid zenity/kdialog handling errors
    742             if opt_gui != 'cli':
    743                 videoTitle = videoTitle.replace('"', '\\"')
    744                 videoTitle = videoTitle.replace("'", "\\'")
    745                 videoTitle = videoTitle.replace('`', '\\`')
    746                 videoTitle = videoTitle.replace("&", "&amp;")
    747                 videoFileName = videoFileName.replace('"', '\\"')
    748                 videoFileName = videoFileName.replace("'", "\\'")
    749                 videoFileName = videoFileName.replace('`', '\\`')
    750                 videoFileName = videoFileName.replace("&", "&amp;")
    751 
    752             # If there is more than one subtitles and opt_selection_mode != 'auto',
    753             # then let the user decide which one will be downloaded
    754             if not subtitlesSelected:
    755                 # Automatic subtitles selection?
    756                 if opt_selection_mode == 'auto':
    757                     subtitlesSelected = selectionAuto(subtitlesList)
    758                 else:
    759                     # Go through the list of subtitles and handle 'auto' settings activation
    760                     for item in subtitlesList['data']:
    761                         if opt_selection_match == 'auto':
    762                             if opt_search_mode == 'hash_and_filename':
    763                                 opt_selection_match = 'on'
    764                         if opt_selection_language == 'auto':
    765                             if searchLanguage > 1:
    766                                 opt_selection_language = 'on'
    767                         if opt_selection_hi == 'auto':
    768                             if item['SubHearingImpaired'] == '1':
    769                                 opt_selection_hi = 'on'
    770                         if opt_selection_rating == 'auto':
    771                             if item['SubRating'] != '0.0':
    772                                 opt_selection_rating = 'on'
    773                         if opt_selection_count == 'auto':
    774                             opt_selection_count = 'on'
    775 
    776                     # Spaw selection window
    777                     if opt_gui == 'gnome':
    778                         subtitlesSelected = selectionGnome(subtitlesList)
    779                     elif opt_gui == 'kde':
    780                         subtitlesSelected = selectionKde(subtitlesList)
    781                     else: # CLI
    782                         subtitlesSelected = selectionCLI(subtitlesList)
    783 
    784             # If a subtitles has been selected at this point, download it!
    785             if subtitlesSelected:
    786                 subIndex = 0
    787                 subIndexTemp = 0
    788 
    789                 # Select the subtitles file to download
    790                 for item in subtitlesList['data']:
    791                     if item['SubFileName'] == subtitlesSelected:
    792                         subIndex = subIndexTemp
    793                         break
    794                     else:
    795                         subIndexTemp += 1
    796 
    797                 subLangId = opt_language_separator + SubLanguageID
    798                 subLangName = subtitlesList['data'][subIndex]['LanguageName']
    799                 subURL = subtitlesList['data'][subIndex]['SubDownloadLink']
    800                 subEncoding = subtitlesList['data'][subIndex]['SubEncoding']
    801 
    802                 subPath = videoPath.rsplit('.', 1)[0] + '.' + subtitlesList['data'][subIndex]['SubFormat']
    803                 if opt_output_path and os.path.isdir(os.path.abspath(opt_output_path)):
    804                     subPath = os.path.abspath(opt_output_path) + "/" + subPath.rsplit('/', 1)[1]
    805 
    806                 # Write language code into the filename?
    807                 if ((opt_language_suffix == 'on') or (opt_language_suffix == 'auto' and searchLanguageResult > 1)):
    808                     subPath = subPath.rsplit('.', 1)[0] + subLangId + '.' + subtitlesList['data'][subIndex]['SubFormat']
    809 
    810                 # Escape non-alphanumeric characters from the subtitles path
    811                 if opt_gui != 'cli':
    812                     subPath = re.escape(subPath)
    813                     subPath = subPath.replace('"', '\\"')
    814                     subPath = subPath.replace("'", "\\'")
    815                     subPath = subPath.replace('`', '\\`')
    816 
    817                 # Make sure we are downloading an UTF8 encoded file
    818                 if opt_force_utf8:
    819                     downloadPos = subURL.find("download/")
    820                     if downloadPos > 0:
    821                         subURL = subURL[:downloadPos+9] + "subencoding-utf8/" + subURL[downloadPos+9:]
    822 
    823                 ## Download and unzip the selected subtitles (with progressbar)
    824                 if opt_gui == 'gnome':
    825                     process_subtitlesDownload = subprocess.call("(wget -q -O - " + subURL + " | gunzip > " + subPath + ") 2>&1" + ' | (zenity --auto-close --progress --pulsate --title="Downloading subtitles, please wait..." --text="Downloading <b>' + subtitlesList['data'][subIndex]['LanguageName'] + '</b> subtitles for <b>' + videoTitle + '</b>...")', shell=True)
    826                 elif opt_gui == 'kde':
    827                     process_subtitlesDownload = subprocess.call("(wget -q -O - " + subURL + " | gunzip > " + subPath + ") 2>&1", shell=True)
    828                 else: # CLI
    829                     print(">> Downloading '" + subtitlesList['data'][subIndex]['LanguageName'] + "' subtitles for '" + videoTitle + "'")
    830 
    831                     if sys.version_info >= (3, 0):
    832                         tmpFile1, headers = urllib.request.urlretrieve(subURL)
    833                         tmpFile2 = gzip.GzipFile(tmpFile1)
    834                         byteswritten = open(subPath, 'wb').write(tmpFile2.read())
    835                         if byteswritten > 0:
    836                             process_subtitlesDownload = 0
    837                         else:
    838                             process_subtitlesDownload = 1
    839                     else: # python 2
    840                         tmpFile1, headers = urllib.urlretrieve(subURL)
    841                         tmpFile2 = gzip.GzipFile(tmpFile1)
    842                         open(subPath, 'wb').write(tmpFile2.read())
    843                         process_subtitlesDownload = 0
    844 
    845                 # If an error occurs, say so
    846                 if process_subtitlesDownload != 0:
    847                     superPrint("error", "Subtitling error!", "An error occurred while downloading or writing <b>" + subtitlesList['data'][subIndex]['LanguageName'] + "</b> subtitles for <b>" + videoTitle + "</b>.")
    848                     osd_server.LogOut(session['token'])
    849                     sys.exit(2)
    850 
    851     ## Print a message if no subtitles have been found, for any of the languages
    852     if searchLanguageResult == 0:
    853         superPrint("info", "No subtitles available :-(", '<b>No subtitles found</b> for this video:\n<i>' + videoFileName + '</i>')
    854         ExitCode = 1
    855     else:
    856         ExitCode = 0
    857 
    858 except (OSError, IOError, RuntimeError, TypeError, NameError, KeyError):
    859 
    860     # Do not warn about remote disconnection # bug/feature of python 3.5?
    861     if "http.client.RemoteDisconnected" in str(sys.exc_info()[0]):
    862         sys.exit(ExitCode)
    863 
    864     # An unknown error occur, let's apologize before exiting
    865     superPrint("error", "Unexpected error!",
    866         "OpenSubtitlesDownload encountered an <b>unknown error</b>, sorry about that...\n\n" + \
    867         "Error: <b>" + str(sys.exc_info()[0]).replace('<', '[').replace('>', ']') + "</b>\n" + \
    868         "Line: <b>" + str(sys.exc_info()[-1].tb_lineno) + "</b>\n\n" + \
    869         "Just to be safe, please check:\n- www.opensubtitles.org availability\" + \n" + \
    870         "- Your downloads limit (200 subtitles per 24h)\n" + \
    871         "- Your Internet connection status\n" + \
    872         "- That are using the latest version of this software ;-)")
    873 
    874 except Exception:
    875 
    876     # Catch unhandled exceptions but do not spawn an error window
    877     print("Unexpected error (line " + str(sys.exc_info()[-1].tb_lineno) + "): " + str(sys.exc_info()[0]))
    878 
    879 # Disconnect from opensubtitles.org server, then exit
    880 if session and session['token']:
    881     osd_server.LogOut(session['token'])
    882 
    883 sys.exit(ExitCode)