2015-08-02 22:47:42 +03:00
|
|
|
'''
|
|
|
|
Created on Apr 2, 2015
|
|
|
|
|
|
|
|
@author: ivan
|
2015-08-06 00:38:19 +03:00
|
|
|
|
2015-08-02 22:47:42 +03:00
|
|
|
import xmlrpclib
|
|
|
|
import urllib2
|
|
|
|
import os.path
|
|
|
|
import gzip
|
|
|
|
from StringIO import StringIO
|
|
|
|
import logging
|
|
|
|
import subprocess
|
|
|
|
import struct
|
|
|
|
import time
|
|
|
|
|
|
|
|
logger = logging.getLogger('opensubtitles')
|
|
|
|
|
|
|
|
|
|
|
|
class Urllib2Transport(xmlrpclib.Transport):
|
|
|
|
def __init__(self, opener=None, https=False, use_datetime=0):
|
|
|
|
xmlrpclib.Transport.__init__(self, use_datetime)
|
|
|
|
self.opener = opener or urllib2.build_opener()
|
|
|
|
self.https = https
|
|
|
|
|
|
|
|
def request(self, host, handler, request_body, verbose=0):
|
|
|
|
proto = ('http', 'https')[bool(self.https)]
|
|
|
|
req = urllib2.Request('%s://%s%s' % (proto, host, handler), request_body)
|
|
|
|
req.add_header('User-agent', self.user_agent)
|
|
|
|
self.verbose = verbose
|
|
|
|
return self.parse_response(self.opener.open(req))
|
|
|
|
|
|
|
|
|
|
|
|
class OpenSubtitles(object):
|
2015-08-03 20:49:05 +03:00
|
|
|
USER_AGENT = 'BTClient'
|
2015-08-02 22:47:42 +03:00
|
|
|
|
|
|
|
def __init__(self, lang, user='', pwd=''):
|
|
|
|
self._lang = lang
|
|
|
|
self._proxy = xmlrpclib.ServerProxy('http://api.opensubtitles.org/xml-rpc',
|
|
|
|
Urllib2Transport(use_datetime=True),
|
|
|
|
allow_none=True, use_datetime=True)
|
|
|
|
self._token = None
|
|
|
|
self._user = user
|
|
|
|
self._pwd = pwd
|
|
|
|
|
|
|
|
def login(self):
|
|
|
|
res = self._proxy.LogIn(self._user, self._pwd, 'en', self.USER_AGENT)
|
|
|
|
self._parse_status(res)
|
|
|
|
token = res.get('token')
|
|
|
|
if token:
|
|
|
|
self._token = token
|
|
|
|
else:
|
|
|
|
raise xmlrpclib.Fault('NO_TOKEN', 'No token!')
|
|
|
|
|
|
|
|
def _parse_status(self, res):
|
|
|
|
if res.has_key('status'):
|
|
|
|
code = res['status'].split()[0]
|
|
|
|
if code != '200':
|
|
|
|
raise xmlrpclib.Fault('ERROR_CODE_RETURENED', 'Returned error status: %s (%s)' % (code, res))
|
|
|
|
return True
|
|
|
|
else:
|
|
|
|
raise xmlrpclib.Fault('NO_STATUS', 'No status!')
|
|
|
|
|
|
|
|
def search(self, filename, filesize=None, filehash=None, limit=20):
|
|
|
|
filename = os.path.split(filename)[1]
|
|
|
|
name = os.path.splitext(filename)[0]
|
|
|
|
query = []
|
|
|
|
if filehash and filesize:
|
|
|
|
query.append({'sublanguageid': self._lang, 'moviehash': filehash, 'moviebytesize': str(filesize)})
|
|
|
|
query.append({'sublanguageid': self._lang, 'tag': filename})
|
|
|
|
query.append({'sublanguageid': self._lang, 'query': name})
|
|
|
|
res = self._proxy.SearchSubtitles(self._token, query, {'limit': limit})
|
|
|
|
self._parse_status(res)
|
|
|
|
data = res.get('data')
|
|
|
|
|
|
|
|
return data if data else []
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _sub_file(filename, lang, ext):
|
|
|
|
lang = lang.lower()
|
|
|
|
path, fname = os.path.split(filename)
|
|
|
|
fname = os.path.splitext(fname)[0]
|
|
|
|
return os.path.join(path, fname + '.' + lang + '.' + ext)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def _base_name(filename):
|
|
|
|
fname = os.path.split(filename)[1]
|
|
|
|
return os.path.splitext(fname)[0]
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def hash_file(f, filesize):
|
|
|
|
|
|
|
|
longlongformat = '<q' # little-endian long long
|
|
|
|
bytesize = struct.calcsize(longlongformat)
|
|
|
|
|
|
|
|
hash = filesize # @ReservedAssignment
|
|
|
|
if filesize < 65536 * 2:
|
|
|
|
raise ValueError("SizeError")
|
|
|
|
|
|
|
|
for _x in range(65536 / bytesize):
|
|
|
|
buffer = f.read(bytesize) # @ReservedAssignment
|
|
|
|
(l_value,) = struct.unpack(longlongformat, buffer)
|
|
|
|
hash += l_value
|
|
|
|
hash = hash & 0xFFFFFFFFFFFFFFFF # to remain as 64bit number @ReservedAssignment
|
|
|
|
|
|
|
|
f.seek(max(0, filesize - 65536))
|
|
|
|
for _x in range(65536 / bytesize):
|
|
|
|
buffer = f.read(bytesize) # @ReservedAssignment
|
|
|
|
(l_value,) = struct.unpack(longlongformat, buffer)
|
|
|
|
hash += l_value
|
|
|
|
hash = hash & 0xFFFFFFFFFFFFFFFF # @ReservedAssignment
|
|
|
|
returnedhash = "%016x" % hash
|
|
|
|
return returnedhash
|
|
|
|
|
|
|
|
def choose(self, data):
|
|
|
|
null = f = open(os.devnull, "w")
|
|
|
|
items = []
|
|
|
|
for l in data:
|
|
|
|
items.append(l['SubDownloadLink'])
|
|
|
|
items.append(l['SubFileName'])
|
|
|
|
items.append(l['SubDownloadsCnt'])
|
|
|
|
|
|
|
|
p = subprocess.Popen(
|
|
|
|
'zenity --list --title "Select subtitles" --text "Select best matching subtitles" --width 1024 --height 600 --column Link --column Name --column Downloads --hide-column=1',
|
|
|
|
stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=null, shell=True)
|
|
|
|
res, _ = p.communicate(u'\n'.join(items).encode('utf-8'))
|
|
|
|
null.close()
|
|
|
|
return res if res.startswith('http') else None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def download_if_not_exists(filename, lang, filesize=None, filehash=None, sub_ext='srt',
|
|
|
|
can_choose=True, overwrite=False, retries=3):
|
|
|
|
sfile = OpenSubtitles._sub_file(filename, lang, sub_ext)
|
|
|
|
if os.path.exists(sfile) and os.stat(sfile).st_size > 0 and not overwrite:
|
|
|
|
logger.debug('subs %s are already downloaded', sfile)
|
|
|
|
return sfile
|
|
|
|
else:
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
with OpenSubtitles(lang) as opensub:
|
|
|
|
res = opensub.download(filename, filesize, filehash, can_choose)
|
|
|
|
if res:
|
|
|
|
logger.debug('Subtitles %s downloaded', res)
|
|
|
|
return res
|
|
|
|
else:
|
|
|
|
logger.debug('No subtitles found for file %s in language %s', filename, lang)
|
|
|
|
return
|
|
|
|
except urllib2.HTTPError, e:
|
|
|
|
retries -= 1
|
|
|
|
if retries <= 0:
|
|
|
|
raise e
|
|
|
|
logger.debug('Retrying to load subtitles due to HTTP error %d, remains %d attempts', e.code,
|
|
|
|
retries)
|
|
|
|
time.sleep(1)
|
|
|
|
|
|
|
|
def download(self, filename, filesize=None, filehash=None, can_choose=True):
|
|
|
|
data = self.search(filename, filesize, filehash)
|
|
|
|
if not data:
|
|
|
|
return None
|
|
|
|
media_file = OpenSubtitles._base_name(filename).lower()
|
|
|
|
|
|
|
|
def same_name(b):
|
|
|
|
return media_file == OpenSubtitles._base_name(b['SubFileName']).lower()
|
|
|
|
|
|
|
|
if filehash and filesize:
|
|
|
|
match = filter(lambda x: x.get('QueryNumber', 0) == 0, data)
|
|
|
|
logger.debug('Got results by filehash')
|
|
|
|
else:
|
|
|
|
match = filter(same_name, data)
|
|
|
|
if match and can_choose != 'always':
|
|
|
|
sub = match[0]
|
|
|
|
link = sub['SubDownloadLink']
|
|
|
|
ext = sub['SubFormat']
|
|
|
|
logger.debug('Find exact match for media file, subtitle is %s', sub['SubFileName'])
|
|
|
|
elif can_choose:
|
|
|
|
link = self.choose(data)
|
|
|
|
ext = 'srt'
|
|
|
|
else:
|
|
|
|
sub = data[0]
|
|
|
|
link = sub['SubDownloadLink']
|
|
|
|
ext = sub['SubFormat']
|
|
|
|
if link:
|
|
|
|
return self.download_link(filename, link, ext)
|
|
|
|
|
|
|
|
def download_link(self, filename, link, ext):
|
|
|
|
out_file = OpenSubtitles._sub_file(filename, self._lang, ext)
|
|
|
|
res = urllib2.urlopen(link, timeout=10)
|
|
|
|
data = StringIO(res.read())
|
|
|
|
data.seek(0)
|
|
|
|
res.close()
|
|
|
|
z = gzip.GzipFile(fileobj=data)
|
|
|
|
with open(out_file, 'wb') as f:
|
|
|
|
while True:
|
|
|
|
d = z.read(1024)
|
|
|
|
if not d:
|
|
|
|
break
|
|
|
|
f.write(d)
|
|
|
|
z.close()
|
|
|
|
return out_file
|
|
|
|
|
|
|
|
def logout(self):
|
|
|
|
try:
|
|
|
|
res = self._proxy.LogOut(self._token)
|
|
|
|
self._parse_status(res)
|
|
|
|
except urllib2.HTTPError:
|
|
|
|
logger.warn('Failed to logout')
|
|
|
|
|
|
|
|
def __enter__(self):
|
|
|
|
self.login()
|
|
|
|
return self
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
|
|
self.logout()
|
|
|
|
|
|
|
|
|
|
|
|
def down(f, lang, overwrite=False):
|
|
|
|
filesize, filehash = calc_hash(f)
|
|
|
|
OpenSubtitles.download_if_not_exists(f, lang, filesize=filesize,
|
|
|
|
filehash=filehash, can_choose=True, overwrite=overwrite)
|
|
|
|
|
|
|
|
|
|
|
|
def calc_hash(f):
|
|
|
|
if not os.access(f, os.R_OK):
|
|
|
|
raise ValueError('Cannot read from file %s' % f)
|
|
|
|
filesize = os.stat(f).st_size
|
|
|
|
with open(f, 'rb') as fs:
|
|
|
|
filehash = OpenSubtitles.hash_file(fs, filesize)
|
|
|
|
return filesize, filehash
|
|
|
|
|
|
|
|
|
|
|
|
def list_subs(f, lang):
|
|
|
|
import pprint
|
|
|
|
|
|
|
|
filesize, filehash = calc_hash(f)
|
|
|
|
with OpenSubtitles(lang) as opensub:
|
|
|
|
res = opensub.search(f, filesize, filehash)
|
|
|
|
res = map(lambda x: {'SubFileName': x['SubFileName'],
|
|
|
|
'SubDownloadsCnt': x['SubDownloadsCnt'],
|
|
|
|
'QueryNumber': x.get('QueryNumber', 0),
|
|
|
|
},
|
|
|
|
res)
|
|
|
|
pprint.pprint(res)
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
|
|
|
|
p = ArgumentParser()
|
|
|
|
p.add_argument("video_file", help="video file")
|
|
|
|
p.add_argument("-d", "--download", action="store_true", help="Download subtitles for video files")
|
|
|
|
p.add_argument("-l", "--list", action="store_true", help="List available subtitles")
|
|
|
|
p.add_argument("--lang", default='eng', help="Language")
|
|
|
|
p.add_argument("--debug", action="store_true", help="Print debug messages")
|
|
|
|
p.add_argument("--overwrite", action="store_true", help="Overwrite existing subtitles ")
|
|
|
|
args = p.parse_args()
|
|
|
|
if args.debug:
|
|
|
|
logging.basicConfig(level=logging.DEBUG)
|
|
|
|
if args.download:
|
|
|
|
down(args.video_file, args.lang, args.overwrite)
|
|
|
|
else:
|
|
|
|
list_subs(args.video_file, args.lang)
|
2015-08-06 00:38:19 +03:00
|
|
|
'''
|