From aad4571164533ca8aeb83126603d0d78aefe0ff8 Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 11:32:18 +0100 Subject: [PATCH 01/13] Use requests for multi-part POST --- ImageUploader.py | 60 ++++++++++++------------- MultipartPostHandler.py | 99 ----------------------------------------- Screenshots.py | 6 +-- pythonbits.py | 4 +- 4 files changed, 33 insertions(+), 136 deletions(-) delete mode 100644 MultipartPostHandler.py diff --git a/ImageUploader.py b/ImageUploader.py index 3f5461c..99c2e85 100644 --- a/ImageUploader.py +++ b/ImageUploader.py @@ -6,35 +6,31 @@ Created by Ichabond on 2012-07-01. """ -import json -import urllib2 -import MultipartPostHandler -import re - - -class Upload(object): - def __init__(self, filelist): - self.images = filelist - self.imageurl = [] - - def upload(self): - opener = urllib2.build_opener(MultipartPostHandler.MultipartPostHandler) - matcher = re.compile(r'http(s)*://') - try: - for image in self.images: - if matcher.match(image): - params = ({'url': image}) - else: - params = ({'ImageUp': open(image, "rb")}) - socket = opener.open("https://images.baconbits.org/upload.php", params) - json_str = socket.read() - if hasattr(json, 'loads') or hasattr(json, 'read'): - read = json.loads(json_str) - else: - err_msg = "I cannot decipher the provided json\n" + \ - "Please report the following output to the relevant bB forum: \n" + \ - ("%s" % dir(json)) - self.imageurl.append("https://images.baconbits.org/images/" + read["ImgName"]) - except Exception as e: - print e - return self.imageurl \ No newline at end of file +import requests + + +BASE_URL = 'https://images.baconbits.org/' + + +class BaconBitsImageUploadError(Exception): + pass + + +def upload(file_or_url): + if any(file_or_url.startswith(schema) for schema in + ('http://', 'https://')): + files = {'url': file_or_url} + else: + files = {'ImageUp': open(file_or_url, 'rb')} + + try: + j = requests.post(BASE_URL + 'upload.php', + files=files).json() + except ValueError: + raise BaconBitsImageUploadError("Failed to upload '%s'!" % file_or_url) + + if 'ImgName' in j: + return BASE_URL + 'images/' + j['ImgName'] + else: + raise BaconBitsImageUploadError("Failed to upload '%s'!" % file_or_url, + repr(j)) diff --git a/MultipartPostHandler.py b/MultipartPostHandler.py deleted file mode 100644 index e0d72de..0000000 --- a/MultipartPostHandler.py +++ /dev/null @@ -1,99 +0,0 @@ -# coding = -""" -Usage: - Enables the use of multipart/form-data for posting forms - -Inspirations: - Upload files in python: - http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306 - urllib2_file: - Fabien Seisen: - -Example: - import MultipartPostHandler, urllib2, cookielib - - cookies = cookielib.CookieJar() - opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookies), - MultipartPostHandler.MultipartPostHandler) - params = { "username" : "bob", "password" : "riviera", - "file" : open("filename", "rb") } - opener.open("http://wwww.bobsite.com/upload/", params) - -Further Example: - The main function of this file is a sample which downloads a page and - then uploads it to the W3C validator. -""" - -import urllib -import urllib2 -import mimetools -import mimetypes -import os -import stat - - -class Callable: - def __init__(self, anycallable): - self.__call__ = anycallable - -# Controls how sequences are uncoded. If true, elements may be given multiple values by -# assigning a sequence. -doseq = 1 - - -class MultipartPostHandler(urllib2.BaseHandler): - handler_order = urllib2.HTTPHandler.handler_order - 10 # needs to run first - - def http_request(self, request): - data = request.get_data() - if data is not None and type(data) != str: - v_files = [] - v_vars = [] - try: - for (key, value) in data.items(): - if type(value) == file: - v_files.append((key, value)) - else: - v_vars.append((key, value)) - except TypeError: - systype, value, traceback = sys.exc_info() - raise TypeError, "not a valid non-string sequence or mapping object", traceback - - if len(v_files) == 0: - data = urllib.urlencode(v_vars, doseq) - else: - boundary, data = self.multipart_encode(v_vars, v_files) - contenttype = 'multipart/form-data; boundary=%s' % boundary - if (request.has_header('Content-Type') - and request.get_header('Content-Type').find('multipart/form-data') != 0): - print "Replacing %s with %s" % (request.get_header('content-type'), 'multipart/form-data') - request.add_unredirected_header('Content-Type', contenttype) - - request.add_data(data) - return request - - def multipart_encode(vars, files, boundary=None, buffer=None): - if boundary is None: - boundary = mimetools.choose_boundary() - if buffer is None: - buffer = '' - for (key, value) in vars: - buffer += '--%s\r\n' % boundary - buffer += 'Content-Disposition: form-data; name="%s"' % key - buffer += '\r\n\r\n' + value + '\r\n' - for (key, fd) in files: - file_size = os.fstat(fd.fileno())[stat.ST_SIZE] - filename = os.path.basename(fd.name) - contenttype = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - buffer += '--%s\r\n' % boundary - buffer += 'Content-Disposition: form-data; name="%s"; filename="%s"\r\n' % (key, filename) - buffer += 'Content-Type: %s\r\n' % contenttype - # buffer += 'Content-Length: %s\r\n' % file_size - fd.seek(0) - buffer += '\r\n' + fd.read() + '\r\n' - buffer += '--%s--\r\n\r\n' % boundary - return boundary, buffer - - multipart_encode = Callable(multipart_encode) - - https_request = http_request \ No newline at end of file diff --git a/Screenshots.py b/Screenshots.py index 218ecbc..9a540b2 100644 --- a/Screenshots.py +++ b/Screenshots.py @@ -8,12 +8,12 @@ """ from Ffmpeg import FFMpeg -from ImageUploader import Upload +from ImageUploader import upload def createScreenshots(file, shots=2): ffmpeg = FFMpeg(file) images = ffmpeg.takeScreenshots(shots) - urls = Upload(images).upload() + urls = map(upload, images) - return urls \ No newline at end of file + return urls diff --git a/pythonbits.py b/pythonbits.py index 4a039ec..59ee461 100755 --- a/pythonbits.py +++ b/pythonbits.py @@ -18,7 +18,7 @@ from Screenshots import createScreenshots -from ImageUploader import Upload +from ImageUploader import upload from optparse import OptionParser @@ -160,7 +160,7 @@ def main(argv): print "Mediainfo: \n", mediainfo for shot in screenshot: print "Screenshot: %s" % shot - cover = Upload([summary['cover']]).upload() + cover = upload(summary['cover']) if cover: print "Image (Optional): ", cover[0] From 60e01a3ddc7bf21032e31b9a8f3a916b1292fd81 Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 12:29:48 +0100 Subject: [PATCH 02/13] Cleaned up exception handling and added a nicer way to create dump file --- Ffmpeg.py | 99 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 54 insertions(+), 45 deletions(-) diff --git a/Ffmpeg.py b/Ffmpeg.py index 4452221..e0edd25 100644 --- a/Ffmpeg.py +++ b/Ffmpeg.py @@ -6,62 +6,71 @@ Created by Ichabond on 2012-07-01. Copyright (c) 2012 Baconseed. All rights reserved. """ - import os -import subprocess -import sys import re +import subprocess +from tempfile import mkdtemp, NamedTemporaryFile -from tempfile import mkdtemp -from hashlib import md5 + +class FFMpegException(Exception): + pass class FFMpeg(object): + def __init__(self, filepath): self.file = filepath - self.ffmpeg = None - self.duration = None - self.tempdir = mkdtemp(prefix="pythonbits-") + os.sep + self.tempdir = mkdtemp(prefix="pythonbits-") - def getDuration(self): - try: - self.ffmpeg = subprocess.Popen([r"ffmpeg", "-i", self.file], stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - except OSError: - sys.stderr.write( - "Error: Ffmpeg not installed, refer to http://www.ffmpeg.org/download.html for installation") - exit(1) - ffmpeg_out = self.ffmpeg.stdout.read() - ffmpeg_duration = re.findall(r'Duration:\D(\d{2}):(\d{2}):(\d{2})', ffmpeg_out) - if not ffmpeg_duration: - # the odds of a filename collision on an md5 digest are very small - out_fn = '%s.txt' % md5(ffmpeg_out).hexdigest() - err_f = open(out_fn, 'wb') - err_f.write(ffmpeg_out) - err_f.close() - err_msg = ("Expected ffmpeg to mention 'Duration' but it did not;\n" + - "Please copy the contents of '%s' to http://pastebin.com/\n" + - " and send the pastebin link to the bB forum.") % out_fn - sys.stderr.write(err_msg) - dur = ffmpeg_duration[0] - dur_hh = int(dur[0]) - dur_mm = int(dur[1]) - dur_ss = int(dur[2]) - self.duration = dur_hh * 3600 + dur_mm * 60 + dur_ss + @property + def duration(self): + ffmpeg_data, __ = ffmpeg_wrapper([r"ffmpeg", "-i", self.file], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + + ffmpeg_duration = re.findall( + r'Duration:\D(\d{2}):(\d{2}):(\d{2})', + ffmpeg_data) + if ffmpeg_duration: + hours, minutes, seconds = map(int, ffmpeg_duration[0]) + return hours * 3600 + minutes * 60 + seconds + else: + self._create_dump_and_panic(ffmpeg_data) + + def _create_dump_and_panic(self, ffmpeg_data): + output_file = NamedTemporaryFile(prefix='ffmpeg-error-dump-', + delete=False, dir='', mode='wb') + with output_file as f: + f.write(ffmpeg_data) + + err_msg = ("Expected ffmpeg to mention 'Duration' but it did not. " + "Please copy the contents of '%s' to http://pastebin.com/ " + "and send the pastebin link to the bB forum." + % output_file.name) + raise FFMpegException(err_msg) def takeScreenshots(self, shots): - self.getDuration() stops = range(20, 81, 60 / (shots - 1)) - imgs = [] - try: - for stop in stops: - imgs.append(self.tempdir + "screen%s.png" % stop) - subprocess.Popen([r"ffmpeg", "-ss", str((self.duration * stop) / 100), "-i", self.file, "-vframes", "1", - "-y", "-f", "image2", "-vf", """scale='max(sar,1)*iw':'max(1/sar,1)*ih'""", imgs[-1]], - stdout=subprocess.PIPE, stderr=subprocess.STDOUT).communicate() - except OSError: - sys.stderr.write( - "Error: Ffmpeg not installed, refer to http://www.ffmpeg.org/download.html for installation") - exit(1) + imgs = [os.path.join(self.tempdir, "screen%s.png" % stop) + for stop in stops] + + duration = self.duration + for img, stop in zip(imgs, stops): + ffmpeg_wrapper([r"ffmpeg", + "-ss", str((duration * stop) / 100), + "-i", self.file, + "-vframes", "1", + "-y", + "-f", + "image2", img], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return imgs + +def ffmpeg_wrapper(*args, **kwargs): + try: + return subprocess.Popen(*args, **kwargs).communicate() + except OSError: + raise FFMpegException("Error: Ffmpeg not installed, refer to " + "http://www.ffmpeg.org/download.html for " + "installation") From 5d5d9b71d6470152a3c62b60fffdf83680f77e90 Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 12:56:42 +0100 Subject: [PATCH 03/13] fix accidental deletion of scale flag sent to ffmpeg --- Ffmpeg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Ffmpeg.py b/Ffmpeg.py index e0edd25..d9be1d5 100644 --- a/Ffmpeg.py +++ b/Ffmpeg.py @@ -61,8 +61,9 @@ def takeScreenshots(self, shots): "-i", self.file, "-vframes", "1", "-y", - "-f", - "image2", img], + "-f", "image2", + r"scale='max(sar,1)*iw':'max(1/sar,1)*ih'", + img], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return imgs From 96c3379cb66f429d7aa4c5617a6ebd6bfe8d44fa Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 17:03:54 +0100 Subject: [PATCH 04/13] Allow for installation of pythonbits via pip. - Renamed and moved modules into a Python package. - Added an __init__.py to make importing smoother. - Added '.txt' to requirements as per pip's documentation. - Created a setup.py. This needs reviewing! - Generated a package manifest. - Modified the README to give the new installation instructions. (This needs checking after pull request!) - Moved the user-facing script into bin directory so that pip will automagically install it in the correct place and chmod it for use. - Updated the shebang line in bin/pythonbits from 'python' to 'python2' to prevent the wrong interpreter being selected on hipster Linux distributions that ship with python3 as system /usr/bin/python. --- MANIFEST | 9 +++++++++ README.md | 8 +++++--- pythonbits.py => bin/pythonbits | 16 +++++----------- Ffmpeg.py => pythonbits/Ffmpeg.py | 0 .../ImageUploader.py | 0 ImdbParser.py => pythonbits/ImdbParser.py | 0 Screenshots.py => pythonbits/Screenshots.py | 0 TvdbParser.py => pythonbits/TvdbParser.py | 0 .../TvdbUnitTests.py | 0 pythonbits/__init__.py | 8 ++++++++ requirements => requirements.txt | 0 setup.py | 19 +++++++++++++++++++ 12 files changed, 46 insertions(+), 14 deletions(-) create mode 100644 MANIFEST rename pythonbits.py => bin/pythonbits (96%) rename Ffmpeg.py => pythonbits/Ffmpeg.py (100%) rename ImageUploader.py => pythonbits/ImageUploader.py (100%) rename ImdbParser.py => pythonbits/ImdbParser.py (100%) rename Screenshots.py => pythonbits/Screenshots.py (100%) rename TvdbParser.py => pythonbits/TvdbParser.py (100%) rename TvdbUnitTests.py => pythonbits/TvdbUnitTests.py (100%) create mode 100644 pythonbits/__init__.py rename requirements => requirements.txt (100%) create mode 100644 setup.py diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..6cc7bbe --- /dev/null +++ b/MANIFEST @@ -0,0 +1,9 @@ +setup.py +bin/pythonbits +pythonbits/Ffmpeg.py +pythonbits/ImageUploader.py +pythonbits/ImdbParser.py +pythonbits/Screenshots.py +pythonbits/TvdbParser.py +pythonbits/TvdbUnitTests.py +pythonbits/__init__.py diff --git a/README.md b/README.md index c989ad6..81d9709 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,11 @@ #### A Python description generator for movies and TV shows ## Install -1. Put all files in your $PATH, and make sure pythonbits.py is executable -2. Use pip (https://github.com/pypa/pip) to install dependencies: pip install -r requirements + $ [sudo] pip install https://github.com/Ichabond/Pythonbits/archive/master.zip + $ pythonbits --help + +Python 2 is required. The correct version of pip may be called `pip2` on some platforms. ## Usage -Use pythonbits.py --help to get a usage overview \ No newline at end of file +Use `pythonbits --help` to get a usage overview diff --git a/pythonbits.py b/bin/pythonbits similarity index 96% rename from pythonbits.py rename to bin/pythonbits index 59ee461..87849d2 100755 --- a/pythonbits.py +++ b/bin/pythonbits @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python2 # encoding: utf-8 """ Pythonbits2.py @@ -12,16 +12,10 @@ import sys import os import subprocess - -import ImdbParser -import TvdbParser - -from Screenshots import createScreenshots - -from ImageUploader import upload - from optparse import OptionParser +from pythonbits import IMDB, TVDB, createScreenshots, upload + def generateSeriesSummary(summary): description = "[b]Description[/b] \n" @@ -127,7 +121,7 @@ def main(argv): for shot in screenshot: print shot elif options.season or options.episode: - tvdb = TvdbParser.TVDB() + tvdb = TVDB() if options.season: tvdb.search(search_string, season=options.season) if options.episode: @@ -144,7 +138,7 @@ def main(argv): summary += "[mediainfo]\n%s\n[/mediainfo]" % mediainfo print summary else: - imdb = ImdbParser.IMDB() + imdb = IMDB() imdb.search(search_string) imdb.movieSelector() summary = imdb.summary() diff --git a/Ffmpeg.py b/pythonbits/Ffmpeg.py similarity index 100% rename from Ffmpeg.py rename to pythonbits/Ffmpeg.py diff --git a/ImageUploader.py b/pythonbits/ImageUploader.py similarity index 100% rename from ImageUploader.py rename to pythonbits/ImageUploader.py diff --git a/ImdbParser.py b/pythonbits/ImdbParser.py similarity index 100% rename from ImdbParser.py rename to pythonbits/ImdbParser.py diff --git a/Screenshots.py b/pythonbits/Screenshots.py similarity index 100% rename from Screenshots.py rename to pythonbits/Screenshots.py diff --git a/TvdbParser.py b/pythonbits/TvdbParser.py similarity index 100% rename from TvdbParser.py rename to pythonbits/TvdbParser.py diff --git a/TvdbUnitTests.py b/pythonbits/TvdbUnitTests.py similarity index 100% rename from TvdbUnitTests.py rename to pythonbits/TvdbUnitTests.py diff --git a/pythonbits/__init__.py b/pythonbits/__init__.py new file mode 100644 index 0000000..0079437 --- /dev/null +++ b/pythonbits/__init__.py @@ -0,0 +1,8 @@ +from ImdbParser import IMDB +from TvdbParser import TVDB +from Screenshots import createScreenshots +from ImageUploader import upload + +__all__ = ['IMDB', 'TVDB', 'createScreenshots', 'upload'] + + diff --git a/requirements b/requirements.txt similarity index 100% rename from requirements rename to requirements.txt diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..54193dd --- /dev/null +++ b/setup.py @@ -0,0 +1,19 @@ +from distutils.core import setup + + +setup( + name='Pythonbits', + author='Ichabond', + version='2.0.0', + packages=['pythonbits'], + scripts=['bin/pythonbits'], + url='https://github.com/Ichabond/Pythonbits', + license='LICENSE', + description='A Python pretty printer for generating attractive movie descriptions with screenshots.', + install_requires=[ + "imdbpie == 1.4.4", + "requests >= 2.3.0", + "tvdb-api == 1.9", + "wsgiref>=0.1.2" + ], +) From 8d8a527fc796fced17efe589043d266805c61134 Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 12:56:42 +0100 Subject: [PATCH 05/13] fix accidental deletion of scale flag sent to ffmpeg --- Ffmpeg.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Ffmpeg.py b/Ffmpeg.py index e0edd25..c64f609 100644 --- a/Ffmpeg.py +++ b/Ffmpeg.py @@ -56,13 +56,8 @@ def takeScreenshots(self, shots): duration = self.duration for img, stop in zip(imgs, stops): - ffmpeg_wrapper([r"ffmpeg", - "-ss", str((duration * stop) / 100), - "-i", self.file, - "-vframes", "1", - "-y", - "-f", - "image2", img], + ffmpeg_wrapper([r"ffmpeg", "-ss", str((duration * stop) / 100), "-i", self.file, "-vframes", "1", + "-y", "-f", "image2", "-vf", """scale='max(sar,1)*iw':'max(1/sar,1)*ih'""", img], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return imgs From ccac228502789904827995e252c855bf1f32c9ca Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 18:13:45 +0100 Subject: [PATCH 06/13] fix the fix to calling ffmpeg --- pythonbits/Ffmpeg.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonbits/Ffmpeg.py b/pythonbits/Ffmpeg.py index d9be1d5..3d66157 100644 --- a/pythonbits/Ffmpeg.py +++ b/pythonbits/Ffmpeg.py @@ -6,6 +6,7 @@ Created by Ichabond on 2012-07-01. Copyright (c) 2012 Baconseed. All rights reserved. """ +import sys import os import re import subprocess @@ -62,7 +63,7 @@ def takeScreenshots(self, shots): "-vframes", "1", "-y", "-f", "image2", - r"scale='max(sar,1)*iw':'max(1/sar,1)*ih'", + "-vf", """scale='max(sar,1)*iw':'max(1/sar,1)*ih'""", img], stdout=subprocess.PIPE, stderr=subprocess.STDOUT) return imgs @@ -71,7 +72,7 @@ def takeScreenshots(self, shots): def ffmpeg_wrapper(*args, **kwargs): try: return subprocess.Popen(*args, **kwargs).communicate() - except OSError: + except OSError as e: raise FFMpegException("Error: Ffmpeg not installed, refer to " "http://www.ffmpeg.org/download.html for " "installation") From f7efcdac060b1712f21f3ec241ccd3c5bd9dc5c7 Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 18:14:52 +0100 Subject: [PATCH 07/13] Added dist/ to git ignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 21bf185..64ca004 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ doc/ etc/ include/ lib/ -man/ \ No newline at end of file +man/ +dist/ From bfa8bc72296f9b7d8cf173625e98d1fe56147a6b Mon Sep 17 00:00:00 2001 From: burkean Date: Mon, 20 Apr 2015 18:17:55 +0100 Subject: [PATCH 08/13] errors out appropriately if the requested file does not exist --- pythonbits/Ffmpeg.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pythonbits/Ffmpeg.py b/pythonbits/Ffmpeg.py index 3d66157..4196835 100644 --- a/pythonbits/Ffmpeg.py +++ b/pythonbits/Ffmpeg.py @@ -21,6 +21,9 @@ class FFMpeg(object): def __init__(self, filepath): self.file = filepath + if not os.path.exists(filepath): + raise FFMpegException("File %s does not exist!" % filepath) + self.tempdir = mkdtemp(prefix="pythonbits-") @property From e37b34015721aa470c545aade2162f310dd1c010 Mon Sep 17 00:00:00 2001 From: burkean Date: Tue, 21 Apr 2015 09:48:48 +0100 Subject: [PATCH 09/13] Added and updated tests. - Added Makefile to automate quick test/install/uninstall & separate requirements.txt for testing dependencies - Added tests for upload function; added mocking dep 'responses' to requirements - Added test cases for taking screenshots with ffmpeg & tiny tiny video as test fixture; added 'mock' to requirements - moved & renamed pythonbits/TvdbUnitTests.py to make the testrunner find it - Updated Tvdb_test to pass, but needs refocusing to test just the interface, not the underlying tvdb lib. --- Makefile | 17 ++++++ pythonbits/test/Ffmpeg_test.py | 56 ++++++++++++++++++ pythonbits/test/ImageUploader_test.py | 56 ++++++++++++++++++ .../{TvdbUnitTests.py => test/Tvdb_test.py} | 25 +++++--- pythonbits/test/video.mp4 | Bin 0 -> 6102 bytes requirements-dev.txt | 5 ++ 6 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 Makefile create mode 100644 pythonbits/test/Ffmpeg_test.py create mode 100644 pythonbits/test/ImageUploader_test.py rename pythonbits/{TvdbUnitTests.py => test/Tvdb_test.py} (85%) create mode 100644 pythonbits/test/video.mp4 create mode 100644 requirements-dev.txt diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..9c9b03a --- /dev/null +++ b/Makefile @@ -0,0 +1,17 @@ +PYTHON=python2 +NOSE=/usr/local/bin/nosetests-2.7 + +all: clean dist install + +dist: + $(PYTHON) setup.py sdist + +install: + pip2 uninstall pythonbits + pip2 install dist/Pythonbits-2.0.0.tar.gz + +clean: + rm -r dist/ + +test: + $(NOSE) --with-progressive --logging-clear-handlers diff --git a/pythonbits/test/Ffmpeg_test.py b/pythonbits/test/Ffmpeg_test.py new file mode 100644 index 0000000..e7e2225 --- /dev/null +++ b/pythonbits/test/Ffmpeg_test.py @@ -0,0 +1,56 @@ +import os +import glob +from tempfile import mkdtemp, NamedTemporaryFile +import re +import subprocess + +import mock +from nose.tools import raises + +from pythonbits.Ffmpeg import FFMpeg, FFMpegException + + +FIXTURE_VIDEO = 'pythonbits/test/video.mp4' +_real_popen = subprocess.Popen + + +def popen_no_environ(*args, **kwargs): + kwargs['env'] = {} + return _real_popen(*args, **kwargs) + + +@mock.patch('subprocess.Popen', new=popen_no_environ) +def test_raises_when_no_ffmpeg_in_path(): + f = NamedTemporaryFile() + try: + FFMpeg(f.name).duration() + except FFMpegException as e: + assert 'Ffmpeg not installed' in str(e) + finally: + f.close() + + +def test_raises_when_file_is_invalid(): + f = NamedTemporaryFile() + try: + FFMpeg(f.name).duration() + except FFMpegException as e: + assert 'Duration' in str(e) + finally: + f.close() + + dumpfiles = glob.glob('ffmpeg-error-dump-*') + assert dumpfiles + for file in dumpfiles: + os.unlink(file) + + +def test_parses_correct_duration_as_int(): + assert FFMpeg(FIXTURE_VIDEO).duration == 5 + + +def test_screenshot_files_are_created(): + shots = 2 + image_files = FFMpeg(FIXTURE_VIDEO).takeScreenshots(shots) + assert len(image_files) == shots + assert all(os.path.exists(image) for image in image_files) diff --git a/pythonbits/test/ImageUploader_test.py b/pythonbits/test/ImageUploader_test.py new file mode 100644 index 0000000..b02ddc8 --- /dev/null +++ b/pythonbits/test/ImageUploader_test.py @@ -0,0 +1,56 @@ +import re +import json + +import responses +from nose.tools import raises + +from pythonbits.ImageUploader import upload, BASE_URL, BaconBitsImageUploadError + + +@responses.activate +def test_upload_from_url_returns_valid_url(): + + def response_json_callback(request): + return (200, + {}, + json.dumps({'ImgName': 'image.jpg'})) + + responses.add_callback(responses.POST, + 'https://images.baconbits.org/upload.php', + callback=response_json_callback, + content_type='application/json') + + s = upload('http://example.com/image.jpg') + assert s == 'https://images.baconbits.org/images/image.jpg' + + +@raises(BaconBitsImageUploadError) +@responses.activate +def test_no_json_or_bad_response_raises_err(): + def response_json_callback(request): + return (404, + {}) + + responses.add_callback(responses.POST, + 'https://images.baconbits.org/upload.php', + callback=response_json_callback, + content_type='application/json') + + upload('http://example.com/image.jpg') + + +@raises(BaconBitsImageUploadError) +@responses.activate +def test_missing_key_raises_err(): + + def response_json_callback(request): + return (200, + {}, + json.dumps({'error': 'badly!'})) + + responses.add_callback(responses.POST, + 'https://images.baconbits.org/upload.php', + callback=response_json_callback, + content_type='application/json') + + upload('http://example.com/image.jpg') diff --git a/pythonbits/TvdbUnitTests.py b/pythonbits/test/Tvdb_test.py similarity index 85% rename from pythonbits/TvdbUnitTests.py rename to pythonbits/test/Tvdb_test.py index 59032ba..1009b68 100644 --- a/pythonbits/TvdbUnitTests.py +++ b/pythonbits/test/Tvdb_test.py @@ -7,7 +7,7 @@ """ import unittest -from TvdbParser import TVDB +from pythonbits.TvdbParser import TVDB class TvdbTest(unittest.TestCase): @@ -23,16 +23,16 @@ def testEpisode(self): self.assertEqual(self.Episode["director"], "Stephen Surjik") self.assertEqual(self.Episode['firstaired'], "2012-06-14") self.assertEqual(self.Episode['writer'], "Matt Nix") - self.assertEqual(len(self.Episode), 26) + self.assertTrue(len(self.Episode) > 26) def testSeason(self): - self.assertEqual(len(self.Season), 10) + self.assertTrue(len(self.Season) > 10) def testShow(self): - self.assertEqual(len(self.Show.data), 25) + self.assertTrue(len(self.Show.data) > 25) self.assertEqual(len(self.Show), 10) - self.assertEqual(self.Show['network'], "ABC") + self.assertIn(self.Show['network'], ["ABC", "NBC"]) self.assertEqual(self.Show['seriesname'], "Scrubs") def testSummaries(self): @@ -44,7 +44,11 @@ def testSummaries(self): 'writer': u'Matt Nix', 'url': u'http://thetvdb.com/?tab=episode&seriesid=80270&seasonid=483302&id=4246443', 'series': u'Burn Notice'} - self.assertEqual(self.tvdb.summary(), BurnNoticeS06E01) + summary = self.tvdb.summary() + for key in BurnNoticeS06E01: + self.assertIn(key, summary) + + self.tvdb.search("Burn Notice", season=5) BurnNoticeS05 = {'episode11': u'Better Halves', 'episode17': u'Acceptable Loss', 'episode15': u'Necessary Evil', 'episode12': u'Dead to Rights', @@ -57,9 +61,14 @@ def testSummaries(self): 'url': u'http://thetvdb.com/?tab=season&seriesid=80270&seasonid=463361', 'series': u'Burn Notice', 'summary': u'Covert intelligence operative Michael Westen has been punched, kicked, choked and shot. And now he\'s received a "burn notice", blacklisting him from the intelligence community and compromising his very identity. He must track down a faceless nemesis without getting himself killed while doubling as a private investigator on the dangerous streets of Miami in order to survive.'} - self.assertEqual(self.tvdb.summary(), BurnNoticeS05) + summary = self.tvdb.summary() + for key in BurnNoticeS05: + self.assertIn(key, summary) + self.tvdb.search("Scrubs") Scrubs = {'rating': u'9.0', 'network': u'ABC', 'series': u'Scrubs', 'contentrating': u'TV-PG', 'summary': u'Scrubs focuses on the lives of several people working at Sacred Heart, a teaching hospital. It features fast-paced dialogue, slapstick, and surreal vignettes presented mostly as the daydreams of the central character, Dr. John Michael "J.D." Dorian.', 'seasons': 10, 'url': u'http://thetvdb.com/?tab=series&id=76156'} - self.assertEqual(self.tvdb.summary(), Scrubs) + summary = self.tvdb.summary() + for key in Scrubs: + self.assertIn(key, summary) diff --git a/pythonbits/test/video.mp4 b/pythonbits/test/video.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..1f625ea570d2174ae1ca9ea381d0f7d3a3a0b807 GIT binary patch literal 6102 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 Date: Tue, 21 Apr 2015 16:07:15 +0100 Subject: [PATCH 10/13] Updated tests for Tvdbparser adapter Replaced tests for Tvdb_test.py with ones that test the interface instead of testing if tvdb_api objects contain the keys and values they contained from the time the tests were first written. Also they are no longer dependent on the network. --- pythonbits/test/Tvdb_test.py | 174 ++++++++++++++++++++++------------- 1 file changed, 112 insertions(+), 62 deletions(-) diff --git a/pythonbits/test/Tvdb_test.py b/pythonbits/test/Tvdb_test.py index 1009b68..809d125 100644 --- a/pythonbits/test/Tvdb_test.py +++ b/pythonbits/test/Tvdb_test.py @@ -7,68 +7,118 @@ """ import unittest +from nose.tools import assert_raises, raises + +import mock +import tvdb_api + from pythonbits.TvdbParser import TVDB -class TvdbTest(unittest.TestCase): - def setUp(self): - self.maxDiff = None - self.tvdb = TVDB() - self.Episode = self.tvdb.search("Burn Notice", episode="S06E01") - self.Season = self.tvdb.search("Burn Notice", season=6) - self.Show = self.tvdb.search("Scrubs") - - def testEpisode(self): - self.assertEqual(self.Episode["episodename"], "Scorched Earth") - self.assertEqual(self.Episode["director"], "Stephen Surjik") - self.assertEqual(self.Episode['firstaired'], "2012-06-14") - self.assertEqual(self.Episode['writer'], "Matt Nix") - self.assertTrue(len(self.Episode) > 26) - - - def testSeason(self): - self.assertTrue(len(self.Season) > 10) - - def testShow(self): - self.assertTrue(len(self.Show.data) > 25) - self.assertEqual(len(self.Show), 10) - self.assertIn(self.Show['network'], ["ABC", "NBC"]) - self.assertEqual(self.Show['seriesname'], "Scrubs") - - def testSummaries(self): - self.tvdb.search("Burn Notice", episode="S06E01") - BurnNoticeS06E01 = {'director': u'Stephen Surjik', 'rating': u'7.9', - 'aired': u'2012-06-14', 'language': u'en', - 'title': u'Scorched Earth', 'genre': u'|Action and Adventure|', - 'summary': u'Michael pursues Anson in Miami. Meanwhile, Fiona is taken into custody and interrogated by a former foe.', - 'writer': u'Matt Nix', - 'url': u'http://thetvdb.com/?tab=episode&seriesid=80270&seasonid=483302&id=4246443', - 'series': u'Burn Notice'} - summary = self.tvdb.summary() - for key in BurnNoticeS06E01: - self.assertIn(key, summary) - - - self.tvdb.search("Burn Notice", season=5) - BurnNoticeS05 = {'episode11': u'Better Halves', 'episode17': u'Acceptable Loss', - 'episode15': u'Necessary Evil', 'episode12': u'Dead to Rights', - 'episode13': u'Damned If You Do', 'episode3': u'Mind Games', - 'episode1': u'Company Man', 'episode10': u'Army of One', 'episodes': 18, - 'episode2': u'Bloodlines', 'episode5': u'Square One', 'episode4': u'No Good Deed', - 'episode7': u'Besieged', 'episode6': u'Enemy of My Enemy', - 'episode9': u'Eye for an Eye', 'episode8': u'Hard Out', 'episode18': u'Fail Safe', - 'episode16': u'Depth Perception', 'episode14': u'Breaking Point', - 'url': u'http://thetvdb.com/?tab=season&seriesid=80270&seasonid=463361', - 'series': u'Burn Notice', - 'summary': u'Covert intelligence operative Michael Westen has been punched, kicked, choked and shot. And now he\'s received a "burn notice", blacklisting him from the intelligence community and compromising his very identity. He must track down a faceless nemesis without getting himself killed while doubling as a private investigator on the dangerous streets of Miami in order to survive.'} - summary = self.tvdb.summary() - for key in BurnNoticeS05: - self.assertIn(key, summary) - - self.tvdb.search("Scrubs") - Scrubs = {'rating': u'9.0', 'network': u'ABC', 'series': u'Scrubs', 'contentrating': u'TV-PG', - 'summary': u'Scrubs focuses on the lives of several people working at Sacred Heart, a teaching hospital. It features fast-paced dialogue, slapstick, and surreal vignettes presented mostly as the daydreams of the central character, Dr. John Michael "J.D." Dorian.', - 'seasons': 10, 'url': u'http://thetvdb.com/?tab=series&id=76156'} - summary = self.tvdb.summary() - for key in Scrubs: - self.assertIn(key, summary) +def test_invalid_episode_identifier_causes_exit(): + tv = TVDB() + tv.tvdb = None + + eps = ("S06 E01","S0601", "601", "S10", "SE10") + for e in eps: + assert_raises(SystemExit, tv.search, "Burn Notice", episode=e) + assert_raises(TypeError, tv.search, "Burn Notice", episode="S01E01") # valid + + +def test_search_for_show_returns_underlying_show(): + # mock out the underlying api call to tvdb_api + tv = TVDB() + tv.tvdb = {'Scrubs': tvdb_api.Show()} + + assert isinstance(tv.search("Scrubs"), tvdb_api.Show) + + +def test_search_for_episode_returns_underlying_episode(): + # mock out the underlying api call to tvdb_api + tv = TVDB() + tv.tvdb = {'Burn Notice': {6: {1: tvdb_api.Episode()}}} + + assert isinstance(tv.search("Burn Notice", episode="S06E01"), tvdb_api.Episode) + + +def test_search_for_season_returns_underlying_season(): + # mock out the underlying api call to tvdb_api + tv = TVDB() + tv.tvdb = {'Burn Notice': {6: tvdb_api.Season()}} + + assert isinstance(tv.search("Burn Notice", season=6), tvdb_api.Season) + + +def test_expected_keys_in_show_summary(): + tv = TVDB() + tv.episode = None + tv.season = None + tv.show = mock_show_builder() + + summary = tv.summary() + expected = ('series', 'seasons', 'network', 'rating', 'contentrating', + 'summary', 'url') + for k in expected: + assert k in summary + + +def mock_show_builder(): + show = tvdb_api.Show() + show['seriesname'] = '' + show['overview'] = '' + show.__len__ = lambda self: 5 + show['network'] = 'ABC' + show['rating'] = '' + show['summary'] = '' + show['contentrating'] = '' + show['id'] = '' + return show + + +def mock_episode_builder(): + episode = tvdb_api.Episode() + episode['episodename'] = '' + episode['director'] = '' + episode['firstaired'] = '' + episode['writer'] = '' + episode['rating'] = '' + episode['overview'] = '' + episode['language'] = '' + episode['seriesid'] = '1' + episode['genre'] = '' + episode['seasonid'] = '' + episode['id'] = '' + return episode + + +def mock_season_builder(): + season = tvdb_api.Season() + season['overview'] = '' + season.episodes = [mock_episode_builder(), mock_episode_builder()] + + +def test_expected_keys_in_episode_summary(): + tv = TVDB() + tv.show = mock_show_builder() + tv.season = None + tv.episode = mock_episode_builder() + tv.tvdb = {1: {'genre': ''}} + + summary = tv.summary() + expected = ('title', 'director', 'aired', 'writer', 'rating', 'summary', 'language', + 'url', 'genre', 'series', 'seriessummary') + for k in expected: + assert k in summary + + +def test_expected_keys_in_season_summary(): + tv = TVDB() + tv.show = mock_show_builder() + tv.season = mock_season_builder() + tv.episode = None + + summary = tv.summary() + expected = ('series', 'url', 'summary') + # doesn't test for the presence of episode\d keys + for k in expected: + assert k in summary, k From 596dbf3d108c2a7b18bfc4255d55dcde045337ed Mon Sep 17 00:00:00 2001 From: burkean Date: Tue, 21 Apr 2015 16:13:48 +0100 Subject: [PATCH 11/13] Removed unused imports from Tvdb_test.py --- pythonbits/test/Tvdb_test.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pythonbits/test/Tvdb_test.py b/pythonbits/test/Tvdb_test.py index 809d125..467677b 100644 --- a/pythonbits/test/Tvdb_test.py +++ b/pythonbits/test/Tvdb_test.py @@ -7,9 +7,8 @@ """ import unittest -from nose.tools import assert_raises, raises +from nose.tools import assert_raises -import mock import tvdb_api from pythonbits.TvdbParser import TVDB From 9e7a8e3fc7246ec87567a230c1a1a35c0c76905d Mon Sep 17 00:00:00 2001 From: burkean Date: Wed, 22 Apr 2015 11:43:31 +0100 Subject: [PATCH 12/13] Rewrote ImdbParser for new imdbpie. - Complete with tests. - Updated requirements & seup.py to pull new imdbpie. - Rewrote ImdbParser module as movie module. - Moved movie summary generating code to movie. - Updated MANIFEST. --- MANIFEST | 3 +- bin/pythonbits | 34 ++---------- pythonbits/ImdbParser.py | 84 ---------------------------- pythonbits/__init__.py | 6 +- pythonbits/movie.py | 102 ++++++++++++++++++++++++++++++++++ pythonbits/test/movie_test.py | 50 +++++++++++++++++ requirements.txt | 2 +- setup.py | 2 +- 8 files changed, 161 insertions(+), 122 deletions(-) delete mode 100644 pythonbits/ImdbParser.py create mode 100644 pythonbits/movie.py create mode 100644 pythonbits/test/movie_test.py diff --git a/MANIFEST b/MANIFEST index 6cc7bbe..3b8984d 100644 --- a/MANIFEST +++ b/MANIFEST @@ -2,8 +2,7 @@ setup.py bin/pythonbits pythonbits/Ffmpeg.py pythonbits/ImageUploader.py -pythonbits/ImdbParser.py +pythonbits/movie.py pythonbits/Screenshots.py pythonbits/TvdbParser.py -pythonbits/TvdbUnitTests.py pythonbits/__init__.py diff --git a/bin/pythonbits b/bin/pythonbits index 87849d2..06c67ef 100755 --- a/bin/pythonbits +++ b/bin/pythonbits @@ -14,7 +14,7 @@ import os import subprocess from optparse import OptionParser -from pythonbits import IMDB, TVDB, createScreenshots, upload +from pythonbits import lookup_movie, TVDB, createScreenshots, upload def generateSeriesSummary(summary): @@ -56,24 +56,6 @@ def generateSeriesSummary(summary): return description -def generateMoviesSummary(summary): - description = "[b]Description[/b] \n" - description += "[quote]%s[/quote]\n" % summary['description'] - description += "[b]Information:[/b]\n" - description += "[quote]IMDB Url: %s\n" % summary['url'] - description += "Title: %s\n" % summary['name'] - description += "Year: %s\n" % summary['year'] - description += "MPAA: %s\n" % summary['mpaa'] - description += "Rating: %s/10\n" % summary['rating'] - description += "Votes: %s\n" % summary['votes'] - description += "Runtime: %s\n" % summary['runtime'] - description += "Director(s): %s\n" % summary['director'] - description += "Writer(s): %s\n" % summary['writers'] - description += "[/quote]" - - return description - - def findMediaInfo(path): mediainfo = None try: @@ -138,23 +120,15 @@ def main(argv): summary += "[mediainfo]\n%s\n[/mediainfo]" % mediainfo print summary else: - imdb = IMDB() - imdb.search(search_string) - imdb.movieSelector() - summary = imdb.summary() - movie = generateMoviesSummary(summary) - print "\n\n\n" - print "Year: ", summary['year'] - print "\n\n\n" - print "Movie Description: \n", movie - print "\n\n\n" + movie = lookup_movie(search_string).get_selection() + print movie.summary if not options.info: mediainfo = findMediaInfo(filename) if mediainfo: print "Mediainfo: \n", mediainfo for shot in screenshot: print "Screenshot: %s" % shot - cover = upload(summary['cover']) + cover = upload(movie.cover_url) if cover: print "Image (Optional): ", cover[0] diff --git a/pythonbits/ImdbParser.py b/pythonbits/ImdbParser.py deleted file mode 100644 index 88ef51b..0000000 --- a/pythonbits/ImdbParser.py +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -""" -ImdbParser.py - -Created by Ichabond on 2012-07-01. - -Module for Pythonbits to provide proper and clean -imdb parsing. - -""" - -import sys -import json - -try: - import imdbpie -except ImportError: - print >> sys.stderr, "IMDbPie is required for Pythonbits to function" - sys.exit(1) - - -class IMDB(object): - def __init__(self): - self.imdb = imdbpie.Imdb() - self.results = None - self.movie = None - - def search(self, title): - try: - results = self.imdb.find_by_title(title) - except imdb.IMDbError, e: - print >> sys.stderr, "You probably don't have an internet connection. Complete error report:" - print >> sys.stderr, e - sys.exit(3) - - self.results = results - - def movieSelector(self): - try: - print "Movies found:" - for (counter, movie) in enumerate(self.results): - outp = u'%s: %s (%s)' % (counter, movie['title'], movie['year']) - print outp - selection = int(raw_input('Select the correct movie [0-%s]: ' % (len(self.results) - 1))) - self.movie = self.imdb.find_movie_by_id(self.results[selection]['imdb_id']) - - except ValueError as e: - try: - selection = int( - raw_input("This is not a correct movie-identifier, try again [0-%s]: " % (len(self.results) - 1))) - self.movie = self.imdb.find_movie_by_id(self.results[selection]['imdb_id']) - except (ValueError, IndexError) as e: - print >> sys.stderr, "You failed" - print >> sys.stderr, e - - except IndexError as e: - try: - selection = int( - raw_input("Your chosen value does not match a movie, try again [0-%s]: " % (len(self.results) - 1))) - self.movie = self.imdb.find_movie_by_id(self.results[selection]['imdb_id']) - except (ValueError, IndexError) as e: - print >> sys.stderr, "You failed" - print >> sys.stderr, e - - def summary(self): - if self.movie: - return {'director': u" | ".join([director.name for director in self.movie.directors_summary]), - 'runtime': self.movie.runtime, 'rating': self.movie.rating, - 'name': self.movie.title, 'votes': self.movie.votes, 'cover': self.movie.cover_url, - 'genre': u" | ".join([genre for genre in self.movie.genres]), - 'writers': u" | ".join([writer.name for writer in self.movie.writers_summary]), - 'mpaa': self.movie.certification, - 'description': self.movie.plot_outline, - 'url': u"http://www.imdb.com/title/%s" % self.movie.imdb_id, - 'year': self.movie.year} - - -if __name__ == "__main__": - imdb = IMDB() - imdb.search("Tinker Tailor Soldier Spy") - imdb.movieSelector() - summary = imdb.summary() - print summary diff --git a/pythonbits/__init__.py b/pythonbits/__init__.py index 0079437..6ef63f6 100644 --- a/pythonbits/__init__.py +++ b/pythonbits/__init__.py @@ -1,8 +1,6 @@ -from ImdbParser import IMDB +from movie import lookup_movie from TvdbParser import TVDB from Screenshots import createScreenshots from ImageUploader import upload -__all__ = ['IMDB', 'TVDB', 'createScreenshots', 'upload'] - - +__all__ = ['lookup_movie', 'TVDB', 'createScreenshots', 'upload'] diff --git a/pythonbits/movie.py b/pythonbits/movie.py new file mode 100644 index 0000000..04dc470 --- /dev/null +++ b/pythonbits/movie.py @@ -0,0 +1,102 @@ +# encoding: utf-8 +import sys +from imdbpie import Imdb + + +class MovieLookUpFailed(Exception): + pass + + +class MovieList(object): + def __init__(self, movies): + self.movies = movies + + def print_movies(self): + for index, movie in enumerate(self.movies, start=1): + print u"%s: %s (%s)" % (index, + movie['title'], + movie['year']) + + def get_selection(self): + self.print_movies() + + while True: + try: + user_choice = int(raw_input(u'Select the correct movie [1-%s]: ' + % len(self))) - 1 + except (IndexError, ValueError): + print >> sys.stderr, u"Bad choice!" + else: + if user_choice >= 0 and user_choice < len(self): + break + else: + print >> sys.stderr, u"Bad choice!" + + return Movie(Imdb().get_title_by_id( + self.movies[user_choice]['imdb_id'])) + + def __len__(self): + return len(self.movies) + + +def lookup_movie(movie_name): + movie_matches = Imdb().search_for_title(movie_name) + if not movie_matches: + raise MovieLookUpFailed("No movies matching this name!") + else: + return MovieList(movie_matches) + + +class Movie(object): + def __init__(self, imdb_info): + self._info = imdb_info + + def __getattr__(self, key): + return getattr(self._info, key) + + @property + def summary(self): + if self.directors_summary: + directors = u"\nDirector(s): {}".format( + u" | ".join(d.name for d in self.directors_summary)) + if self.writers_summary: + writers = u"\nWriter(s): {}".format( + u" | ".join(w.name for w in self.writers_summary)) + + + return self.MOVIE_SUMMARY_TEMPLATE.format( + description=self.plot_outline, + url=u"http://www.imdb.com/title/%s" % self.imdb_id, + title=self.title, + year=self.year, + mpaa=self.certification, + rating=self.rating, + votes=self.votes, + runtime=self.runtime, + directors=locals().get('directors', u''), + writers=locals().get('writers', u'')) + + MOVIE_SUMMARY_TEMPLATE = \ + u""" +[b]Description[/b] +[quote] +{description} +[/quote] +[b]Information:[/b] +[quote] +IMDB Url: {url} +Title: {title} +Year: {year} +MPAA: {mpaa} +Rating: {rating}/10 +Votes: {votes} +Runtime: {runtime}{directors}{writers} +[/quote] + + +Year: {year} + + +Movie Description: +{description} +""" diff --git a/pythonbits/test/movie_test.py b/pythonbits/test/movie_test.py new file mode 100644 index 0000000..0f00a36 --- /dev/null +++ b/pythonbits/test/movie_test.py @@ -0,0 +1,50 @@ +# encoding: utf-8 +from mock import patch, MagicMock +from nose.tools import raises +from StringIO import StringIO +import re + +from pythonbits.movie import (Movie, lookup_movie, + MovieLookUpFailed) + + +sample_movies_list = [{'title': 'x', 'year': '1'}, + {'title': 'x', 'year': '123'}] + + +@patch('imdbpie.Imdb.search_for_title', return_value=sample_movies_list) +def test_movie_list_created_with_appropriate_length(mock): + l = lookup_movie('example') + assert l.movies == sample_movies_list + assert len(l) == 2 + + +@patch('imdbpie.Imdb.search_for_title', return_value=sample_movies_list) +def test_movie_link_print_movies_prints_appropriate_number_of_lines(movie): + l = lookup_movie('example') + with patch('sys.stdout', new=StringIO()) as out: + l.print_movies() + + output = out.getvalue().strip() + assert output.startswith('1') + assert len(output.split('\n')) == 2 + for line in output.split('\n'): + assert re.match(r'\d+\: .*? \(\d+\)', line) + + +@raises(MovieLookUpFailed) +@patch('imdbpie.Imdb.search_for_title', return_value=[]) +def test_lookup_raises_error_when_no_matches(mock): + lookup_movie('example') + + +def test_summary_returns_unicode_object(): + m = MagicMock() + + movie = Movie({}) + with patch.object(movie, '_info', m): + output = movie.summary + + assert 'Year' in output + assert '[/quote]' in output + assert isinstance(output, unicode) diff --git a/requirements.txt b/requirements.txt index 9598f74..b7bc940 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -imdbpie==1.4.4 +imdbpie>=2.0.0 requests==2.3.0 tvdb-api==1.9 wsgiref==0.1.2 diff --git a/setup.py b/setup.py index 54193dd..4fe37da 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ license='LICENSE', description='A Python pretty printer for generating attractive movie descriptions with screenshots.', install_requires=[ - "imdbpie == 1.4.4", + "imdbpie >= 2.0.0", "requests >= 2.3.0", "tvdb-api == 1.9", "wsgiref>=0.1.2" From d8c332b0849c2d5a602d9f660b6e0d54f8330df8 Mon Sep 17 00:00:00 2001 From: burkean Date: Wed, 22 Apr 2015 13:44:34 +0100 Subject: [PATCH 13/13] Updated Makefile: ignore absent or deleted pythonbits for uninstall or delete dist/ --- Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 9c9b03a..d2d3b26 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,6 @@ PYTHON=python2 NOSE=/usr/local/bin/nosetests-2.7 +PIP=/usr/local/bin/pip2 all: clean dist install @@ -7,11 +8,11 @@ dist: $(PYTHON) setup.py sdist install: - pip2 uninstall pythonbits + -pip2 uninstall pythonbits -y pip2 install dist/Pythonbits-2.0.0.tar.gz clean: - rm -r dist/ + -rm -r dist/ test: $(NOSE) --with-progressive --logging-clear-handlers