Library update
This commit is contained in:
@@ -15,11 +15,15 @@
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with subliminal. If not, see <http://www.gnu.org/licenses/>.
|
||||
from ..exceptions import MissingLanguageError, DownloadFailedError
|
||||
from .. import cache
|
||||
from ..exceptions import MissingLanguageError, DownloadFailedError, ServiceError
|
||||
from ..subtitles import EXTENSIONS
|
||||
from guessit.language import lang_set, UNDETERMINED
|
||||
import logging
|
||||
import os
|
||||
import requests
|
||||
import threading
|
||||
import zipfile
|
||||
|
||||
|
||||
__all__ = ['ServiceBase', 'ServiceConfig']
|
||||
@@ -37,7 +41,7 @@ class ServiceBase(object):
|
||||
server_url = ''
|
||||
|
||||
#: User Agent for any HTTP-based requests
|
||||
user_agent = 'subliminal v0.5'
|
||||
user_agent = 'subliminal v0.6'
|
||||
|
||||
#: Whether based on an API or not
|
||||
api_based = False
|
||||
@@ -45,21 +49,18 @@ class ServiceBase(object):
|
||||
#: Timeout for web requests
|
||||
timeout = 5
|
||||
|
||||
#: Lock for cache interactions
|
||||
lock = threading.Lock()
|
||||
|
||||
#: Mapping to Service's language codes and subliminal's
|
||||
languages = {}
|
||||
|
||||
#: Whether the mapping is reverted or not
|
||||
reverted_languages = False
|
||||
|
||||
#: Accepted video classes (:class:`~subliminal.videos.Episode`, :class:`~subliminal.videos.Movie`, :class:`~subliminal.videos.UnknownVideo`)
|
||||
videos = []
|
||||
|
||||
#: Whether the video has to exist or not
|
||||
require_video = False
|
||||
|
||||
#: List of required features for BeautifulSoup
|
||||
required_features = None
|
||||
|
||||
def __init__(self, config=None):
|
||||
self.config = config or ServiceConfig()
|
||||
|
||||
@@ -75,6 +76,30 @@ class ServiceBase(object):
|
||||
logger.debug(u'Initializing %s' % self.__class__.__name__)
|
||||
self.session = requests.session(timeout=10, headers={'User-Agent': self.user_agent})
|
||||
|
||||
def init_cache(self):
|
||||
"""Initialize cache, make sure it is loaded from disk"""
|
||||
if not self.config or not self.config.cache:
|
||||
raise ServiceError('Cache directory is required')
|
||||
|
||||
service_name = self.__class__.__name__
|
||||
self.config.cache.load(service_name)
|
||||
|
||||
def save_cache(self):
|
||||
service_name = self.__class__.__name__
|
||||
self.config.cache.save(service_name)
|
||||
|
||||
def clear_cache(self):
|
||||
service_name = self.__class__.__name__
|
||||
self.config.cache.clear(service_name)
|
||||
|
||||
def cache_for(self, func, args, result):
|
||||
service_name = self.__class__.__name__
|
||||
return self.config.cache.cache_for(service_name, func, args, result)
|
||||
|
||||
def cached_value(self, func, args):
|
||||
service_name = self.__class__.__name__
|
||||
return self.config.cache.cached_value(service_name, func, args)
|
||||
|
||||
def terminate(self):
|
||||
"""Terminate connection"""
|
||||
logger.debug(u'Terminating %s' % self.__class__.__name__)
|
||||
@@ -84,25 +109,20 @@ class ServiceBase(object):
|
||||
pass
|
||||
|
||||
def list(self, video, languages):
|
||||
"""List subtitles"""
|
||||
pass
|
||||
"""List subtitles
|
||||
|
||||
As a service writer, you can either override this method or implement
|
||||
:meth:`list_checked` instead to have the languages pre-filtered for you
|
||||
|
||||
"""
|
||||
if not self.check_validity(video, languages):
|
||||
return []
|
||||
return self.list_checked(video, languages)
|
||||
|
||||
def download(self, subtitle):
|
||||
"""Download a subtitle"""
|
||||
self.download_file(subtitle.link, subtitle.path)
|
||||
|
||||
@classmethod
|
||||
def available_languages(cls):
|
||||
"""Available languages in the Service
|
||||
|
||||
:return: available languages
|
||||
:rtype: set
|
||||
|
||||
"""
|
||||
if not cls.reverted_languages:
|
||||
return set(cls.languages.keys())
|
||||
return set(cls.languages.values())
|
||||
|
||||
@classmethod
|
||||
def check_validity(cls, video, languages):
|
||||
"""Check for video and languages validity in the Service
|
||||
@@ -113,72 +133,15 @@ class ServiceBase(object):
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
languages &= cls.available_languages()
|
||||
languages = (lang_set(languages) & cls.languages) - set([UNDETERMINED])
|
||||
if not languages:
|
||||
logger.debug(u'No language available for service %s' % cls.__class__.__name__.lower())
|
||||
logger.debug(u'No language available for service %s' % cls.__name__.lower())
|
||||
return False
|
||||
if not cls.is_valid_video(video):
|
||||
logger.debug(u'%r is not valid for service %s' % (video, cls.__class__.__name__.lower()))
|
||||
if cls.require_video and not video.exists or not isinstance(video, tuple(cls.videos)):
|
||||
logger.debug(u'%r is not valid for service %s' % (video, cls.__name__.lower()))
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_valid_video(cls, video):
|
||||
"""Check if video is valid in the Service
|
||||
|
||||
:param video: the video to check
|
||||
:type video: :class:`~subliminal.videos.Video`
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if cls.require_video and not video.exists:
|
||||
return False
|
||||
if not isinstance(video, tuple(cls.videos)):
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def is_valid_language(cls, language):
|
||||
"""Check if language is valid in the Service
|
||||
|
||||
:param string language: the language to check
|
||||
:rtype: bool
|
||||
|
||||
"""
|
||||
if language in cls.available_languages():
|
||||
return True
|
||||
return False
|
||||
|
||||
@classmethod
|
||||
def get_revert_language(cls, language):
|
||||
"""ISO-639-1 language code from service language code
|
||||
|
||||
:param string language: service language code
|
||||
:return: ISO-639-1 language code
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if not cls.reverted_languages and language in cls.languages.values():
|
||||
return [k for k, v in cls.languages.iteritems() if v == language][0]
|
||||
if cls.reverted_languages and language in cls.languages.keys():
|
||||
return cls.languages[language]
|
||||
raise MissingLanguageError(language)
|
||||
|
||||
@classmethod
|
||||
def get_language(cls, language):
|
||||
"""Service language code from ISO-639-1 language code
|
||||
|
||||
:param string language: ISO-639-1 language code
|
||||
:return: service language code
|
||||
:rtype: string
|
||||
|
||||
"""
|
||||
if not cls.reverted_languages and language in cls.languages.keys():
|
||||
return cls.languages[language]
|
||||
if cls.reverted_languages and language in cls.languages.values():
|
||||
return [k for k, v in cls.languages.iteritems() if v == language][0]
|
||||
raise MissingLanguageError(language)
|
||||
|
||||
def download_file(self, url, filepath):
|
||||
"""Attempt to download a file and remove it in case of failure
|
||||
|
||||
@@ -198,6 +161,43 @@ class ServiceBase(object):
|
||||
raise DownloadFailedError(str(e))
|
||||
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
|
||||
|
||||
def download_zip_file(self, url, filepath):
|
||||
"""Attempt to download a zip file and extract any subtitle file from it, if any.
|
||||
This cleans up after itself if anything fails.
|
||||
|
||||
:param string url: URL of the zip file to download
|
||||
:param string filepath: destination path for the subtitle
|
||||
|
||||
"""
|
||||
logger.info(u'Downloading %s' % url)
|
||||
try:
|
||||
zippath = filepath + '.zip'
|
||||
r = self.session.get(url, headers={'Referer': url, 'User-Agent': self.user_agent})
|
||||
with open(zippath, 'wb') as f:
|
||||
f.write(r.content)
|
||||
if not zipfile.is_zipfile(zippath):
|
||||
# TODO: could check if maybe we already have a text file and
|
||||
# download it directly
|
||||
raise DownloadFailedError('Downloaded file is not a zip file')
|
||||
zipsub = zipfile.ZipFile(zippath)
|
||||
for subfile in zipsub.namelist():
|
||||
if os.path.splitext(subfile)[1] in EXTENSIONS:
|
||||
open(filepath, 'w').write(zipsub.open(subfile).read())
|
||||
break
|
||||
else:
|
||||
logger.debug(u'No subtitles found in zip file')
|
||||
raise DownloadFailedError('No subtitles found in zip file')
|
||||
os.remove(zippath)
|
||||
logger.debug(u'Download finished for file %s. Size: %s' % (filepath, os.path.getsize(filepath)))
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(u'Download %s failed: %s' % (url, e))
|
||||
if os.path.exists(zippath):
|
||||
os.remove(zippath)
|
||||
if os.path.exists(filepath):
|
||||
os.remove(filepath)
|
||||
raise DownloadFailedError(str(e))
|
||||
|
||||
|
||||
class ServiceConfig(object):
|
||||
"""Configuration for any :class:`Service`
|
||||
@@ -209,6 +209,9 @@ class ServiceConfig(object):
|
||||
def __init__(self, multi=False, cache_dir=None):
|
||||
self.multi = multi
|
||||
self.cache_dir = cache_dir
|
||||
self.cache = None
|
||||
if cache_dir is not None:
|
||||
self.cache = cache.Cache(cache_dir)
|
||||
|
||||
def __repr__(self):
|
||||
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache_dir)
|
||||
return 'ServiceConfig(%r, %s)' % (self.multi, self.cache.cache_dir)
|
||||
|
||||
Reference in New Issue
Block a user