Skip to content

Commit

Permalink
Add support for subtitle search by title and year.
Browse files Browse the repository at this point in the history
  • Loading branch information
alaperrot committed Oct 26, 2014
1 parent 5c525a9 commit 537ee09
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 56 deletions.
2 changes: 1 addition & 1 deletion addon.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="service.subtitles.yifysubtitles" name="YIFY Subtitles" version="1.0.0" provider-name="alaperrot">
<addon id="service.subtitles.yifysubtitles" name="YIFY Subtitles" version="1.2.0" provider-name="alaperrot">
<requires>
<import addon="xbmc.python" version="2.14.0"/>
</requires>
Expand Down
7 changes: 7 additions & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
1.2.0
New stable release.
Added support for subtitle search by title and year of release.

1.1.0
New development release.

1.0.0
First stable release.
Only supports subtitle search by IMDB identifier.
Expand Down
49 changes: 49 additions & 0 deletions resources/lib/omdbapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-


"""
The OMDb API interface.
"""


from contextlib import closing
from json import loads
from urllib import quote_plus
from urllib2 import urlopen


class OMDbAPI:
"""
The OMDb API.
"""

def __init__(self):
self.logger = None
self._base_url = u'http://www.omdbapi.com'

def search(self, title, year):
"""
Search for a movie.
:param title: Movie title
:type title: unicode
:param year: Year of release
:type year: int
:return: IMDB identifier
:rtype: unicode
"""

self.logger.debug(u'Looking for {0} ({1})'.format(title, year))

url = u'{0}/?t={1}&y={2}'.format(self._base_url, quote_plus(title), year)
with closing(urlopen(url)) as f:
response = loads(f.read())

if not response.get('Response', u'False') == u'True':
self.logger.warn(u'No match found for {0} ({1})'.format(title, year))
return None

imdb_id = response['imdbID']
self.logger.debug(u'IMDB identifier {2} found for {0} ({1})'.format(title, year, imdb_id))

return imdb_id
70 changes: 38 additions & 32 deletions resources/lib/yifysubtitles.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# -*- coding: utf-8 -*-


"""
YIFY Subtitles website interface.
"""


from StringIO import StringIO
from abc import abstractmethod, ABCMeta
from contextlib import closing
Expand All @@ -23,31 +28,31 @@ def debug(self, message):
"""Print a debug message.
:param message: Message
:type message: str
:type message: unicode
"""

@abstractmethod
def info(self, message):
"""Print an informative message.
:param message: Message
:type message: str
:type message: unicode
"""

@abstractmethod
def warn(self, message):
"""Print a warning message.
:param message: Message
:type message: str
:type message: unicode
"""

@abstractmethod
def error(self, message):
"""Print an error message.
:param message: Message
:type message: str
:type message: unicode
"""


Expand All @@ -64,15 +69,15 @@ def on_subtitle_found(self, subtitle):
"""Event handler called when a matching subtitle has been found.
:param subtitle: Subtitle details
:type subtitle: dict of [str, str]
:type subtitle: dict of [str, unicode]
"""

@abstractmethod
def on_subtitle_downloaded(self, path):
"""Event handler called when a subtitle has been downloaded and unpacked.
:param path: Subtitle path
:type path: str
:type path: unicode
"""


Expand All @@ -89,29 +94,29 @@ def __init__(self):
""":type: YifySubtitlesLogger"""

self.workdir = None
""":type: str"""
""":type: unicode"""

self._base_url = 'http://www.yifysubtitles.com'
self._base_url = u'http://www.yifysubtitles.com'

def download(self, url, filename):
"""Download a subtitle.
The on_subtitle_download() method of the registered listener will be called for each downloaded subtitle.
:param url: URL to subtitle archive
:type url: str
:type url: unicode
:param filename: Path to subtitle file within the archive
:type filename: str
:type filename: unicode
"""

path = os.path.join(self.workdir, os.path.basename(filename))

self.logger.debug('Downloading subtitle archive from {0}'.format(url))
self.logger.debug(u'Downloading subtitle archive from {0}'.format(url))
with closing(urlopen(url)) as f:
content = StringIO(f.read())

self.logger.debug('Extracting subtitle to {0}'.format(path))
with ZipFile(content) as z, closing(open(path, mode='wb')) as f:
self.logger.debug(u'Extracting subtitle to {0}'.format(path))
with ZipFile(content) as z, closing(open(path.encode('utf-8'), mode='wb')) as f:
f.write(z.read(filename))

self.listener.on_subtitle_downloaded(path)
Expand All @@ -122,19 +127,20 @@ def search(self, imdb_id, languages):
The on_subtitle_found() method of the registered listener will be called for each found subtitle.
:param imdb_id: IMDB identifier
:type imdb_id: str
:type imdb_id: unicode
:param languages: Accepted languages
:type languages: list of str
:type languages: list of unicode
"""

self.logger.debug(u'Searching subtitles for IMDB identifier {0}'.format(imdb_id))
page = self._fetch_movie_page(imdb_id)
self._list_subtitles(page, languages)

def _fetch_movie_page(self, imdb_id):
"""Fetch the movie page for an IMDB identifier.
:param imdb_id: IMDB identifier
:type imdb_id: str
:type imdb_id: unicode
:return: Movie page
:rtype: unicode
"""
Expand All @@ -150,7 +156,7 @@ def _fetch_subtitle_page(self, link):
"""Fetch a subtitle page.
:param link: Relative URL to subtitle page
:type link: str
:type link: unicode
:return: Subtitle page
:rtype: unicode
"""
Expand All @@ -168,7 +174,7 @@ def _list_subtitles(self, page, languages):
:param page: Movie page
:type page: unicode
:param languages: Accepted languages
:type languages: list of str
:type languages: list of unicode
"""

pattern = re.compile(r'<li data-id=".*?"(?: class="((?:high|low)-rating)")?>\s*'
Expand All @@ -184,9 +190,9 @@ def _list_subtitles(self, page, languages):
re.UNICODE)

for match in pattern.findall(page):
language = self._get_subtitle_language(match[2])
page_url = match[1].encode('utf-8')
rating = self._get_subtitle_rating(match[0])
language = self._get_subtitle_language(unicode(match[2]))
page_url = unicode(match[1])
rating = self._get_subtitle_rating(unicode(match[0]))

if language in languages:
page = self._fetch_subtitle_page(page_url)
Expand All @@ -199,13 +205,13 @@ def _list_subtitles(self, page, languages):
})

else:
self.logger.debug('Ignoring {0} subtitle {1}'.format(language, page_url))
self.logger.debug(u'Ignoring {0} subtitle {1}'.format(language, page_url))

def _list_subtitles_archive(self, archive):
"""List subtitles from a ZIP archive.
:param archive: ZIP archive URL
:type archive: dict of [str, str]
:type archive: dict of [str, unicode]
"""

with closing(urlopen(archive['url'])) as f:
Expand All @@ -218,7 +224,7 @@ def _list_subtitles_archive(self, archive):
]

for filename in filenames:
self.logger.debug('Found {0} subtitle {1}:{2}'.format(archive['language'], archive['url'], filename))
self.logger.debug(u'Found {0} subtitle {1}:{2}'.format(archive['language'], archive['url'], filename))
self.listener.on_subtitle_found({
'filename': filename,
'language': archive['language'],
Expand All @@ -233,13 +239,13 @@ def _get_subtitle_language(language):
:param language: Subtitle language
:type language: unicode
:return: XBMC language
:rtype: str
:rtype: unicode
"""

return {
u'Brazilian Portuguese': u'Portuguese (Brazil)',
u'Farsi/Persian': u'Persian',
}.get(language, language).encode('utf-8')
}.get(language, language)

@staticmethod
def _get_subtitle_rating(rating):
Expand All @@ -248,13 +254,13 @@ def _get_subtitle_rating(rating):
:param rating: Subtitles rating
:type rating: unicode
:return: Subtitles rating
:rtype: str
:rtype: unicode
"""

return {
u'high-rating': '5',
u'low-rating': '0',
}.get(rating, '3')
u'high-rating': u'5',
u'low-rating': u'0',
}.get(rating, u'3')

@staticmethod
def _get_subtitle_url(page):
Expand All @@ -263,9 +269,9 @@ def _get_subtitle_url(page):
:param page: Subtitle page
:type page: unicode
:return: Subtitle URL
:rtype: str
:rtype: unicode
"""

pattern = re.compile(r'<a href="([^"]*)" class="[^"]*\bdownload-subtitle\b[^"]*">', re.UNICODE)
match = pattern.search(page)
return str(match.group(1)) if match else None
return unicode(match.group(1)) if match else None
25 changes: 16 additions & 9 deletions resources/lib/yifytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from shutil import rmtree
from tempfile import mkdtemp
from omdbapi import OMDbAPI
from yifysubtitles import YifySubtitles
from yifysubtitles import YifySubtitlesListener
from yifysubtitles import YifySubtitlesLogger
Expand All @@ -12,6 +13,9 @@ class TestService(YifySubtitlesListener, YifySubtitlesLogger):
def __init__(self):
super(TestService, self).__init__()

self._omdbapi = OMDbAPI()
self._omdbapi.logger = self

self._yifysubtitles = YifySubtitles()
self._yifysubtitles.listener = self
self._yifysubtitles.logger = self
Expand All @@ -23,34 +27,37 @@ def __init__(self):
def cleanup(self):
rmtree(self._yifysubtitles.workdir)

def lookup(self, title, year):
return self._omdbapi.search(title, year)

def download(self, url, filename):
self._num_subtitles_downloaded = 0
self._yifysubtitles.download(url, filename)
self.info('{0} subtitles downloaded'.format(self._num_subtitles_downloaded))
self.info(u'{0} subtitles downloaded'.format(self._num_subtitles_downloaded))

def search(self, imdb_id, languages):
self._num_subtitles_found = 0
self._yifysubtitles.search(imdb_id, languages)
self.info('{0} subtitles found'.format(self._num_subtitles_found))
self.info(u'{0} subtitles found'.format(self._num_subtitles_found))

def on_subtitle_found(self, subtitle):
self._num_subtitles_found += 1
self.info('Found {0} subtitle {1}'.format(subtitle['language'], subtitle['filename']))
self.info(u'Found {0} subtitle {1}'.format(subtitle['language'], subtitle['filename']))
for key in subtitle:
self.debug(' {0}: {1}'.format(key, subtitle[key]))
self.debug(u' {0}: {1}'.format(key, subtitle[key]))

def on_subtitle_downloaded(self, path):
self._num_subtitles_downloaded += 1
self.info('Subtitle {0} downloaded'.format(path))
self.info(u'Subtitle {0} downloaded'.format(path))

def debug(self, message):
print 'DEBUG: {0}'.format(message)
print u'DEBUG: {0}'.format(message)

def info(self, message):
print 'INFO: {0}'.format(message)
print u'INFO: {0}'.format(message)

def warn(self, message):
print 'WARN: {0}'.format(message)
print u'WARN: {0}'.format(message)

def error(self, message):
print 'ERROR: {0}'.format(message)
print u'ERROR: {0}'.format(message)
Loading

0 comments on commit 537ee09

Please sign in to comment.