diff --git a/pythonbits/bb.py b/pythonbits/bb.py
index a02f893..54ddf99 100644
--- a/pythonbits/bb.py
+++ b/pythonbits/bb.py
@@ -24,7 +24,8 @@
from .submission import (Submission, form_field, finalize,
SubmissionAttributeError)
from .tracker import Tracker
-from .scene import is_scene_crc, query_scene_fname
+from . import scene
+from . import prompt
def format_tag(tag):
@@ -76,28 +77,48 @@ def submit(payload):
t = Tracker()
return t.upload(**payload)
- @form_field('scene', 'checkbox')
- def _render_scene(self):
- # todo: if path is directory, choose file for crc
- path = os.path.normpath(self['path']) # removes trailing slash
- try:
- try:
- if os.path.exists(path) and not os.path.isdir(path):
- return is_scene_crc(path)
- except KeyboardInterrupt:
- sys.stdout.write('...skipped\n')
-
- query_scene_fname(path)
- except HTTPError as e:
- log.notice(e)
+ # TODO: These should be detected as scene releases and produce an error message
+ # because they must be in a directory named after the release:
- while True:
- choice = input('Is this a scene release? [y/N] ')
+ # voa-the_omega_man_x264_bluray.mkv -> The.Omega.Man.1971.1080p.BluRay.x264-VOA/voa-the_omega_man_x264_bluray.mkv
+ # hd1080-wtl.mkv -> Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080/hd1080-wtl.mkv
- if not choice or choice.lower() == 'n':
- return False
- elif choice.lower() == 'y':
- return True
+ @form_field('scene', 'checkbox')
+ def _render_scene(self):
+ def ask_user():
+ return prompt.yesno('Is this a scene release?', default=False)
+
+ def handle_error(filepath, error):
+ log.error(str(error))
+
+ path = self['path']
+ modified = scene.check_integrity(path, on_error=handle_error)
+ # modified is True, False or None (unknown)
+ if modified is False:
+ return True
+ elif modified is True:
+ if prompt.yesno('Abort?', default=True):
+ # The return statement is there because sys.exit() is mocked in tests.
+ log.debug('Aborting')
+ return sys.exit(1)
+ else:
+ log.debug('Asking user')
+ return ask_user()
+
+ # Check if release was renamed
+ release_names = scene.release_names(path)
+ if not release_names:
+ return False
+ elif release_names:
+ print('Existing releases:')
+ for release_name in release_names:
+ print(' * {}'.format(release_name))
+ log.error('This release was very likely modified and should not be uploaded like this.')
+ if prompt.yesno('Abort?', default=True):
+ # The return statement is there because sys.exit() is mocked in tests.
+ return sys.exit(1)
+ else:
+ return ask_user()
def data_method(self, source, target):
def copy(source, target):
diff --git a/pythonbits/prompt.py b/pythonbits/prompt.py
new file mode 100644
index 0000000..5583cde
--- /dev/null
+++ b/pythonbits/prompt.py
@@ -0,0 +1,13 @@
+# -*- coding: utf-8 -*-
+
+
+def yesno(question, default):
+ while True:
+ choices = '[Y/n]' if default else '[y/N]'
+ choice = input('%s %s ' % (question, choices))
+ if not choice:
+ return default
+ elif choice.casefold() == 'y':
+ return True
+ elif choice.casefold() == 'n':
+ return False
diff --git a/pythonbits/scene.py b/pythonbits/scene.py
index 90345e6..6f6618c 100644
--- a/pythonbits/scene.py
+++ b/pythonbits/scene.py
@@ -1,81 +1,382 @@
# -*- coding: utf-8 -*-
import os
import requests
-import progressbar
from base64 import b64decode
-from zlib import crc32
+from bs4 import BeautifulSoup
+import re
+import guessit
+import functools
+import pprint
from .logging import log
+
srrdb = b64decode('aHR0cHM6Ly9zcnJkYi5jb20v').decode('utf8')
-def check_scene_rename(fname, release):
- release_url = srrdb + "release/details/{}".format(release)
- r = requests.get(release_url)
-
- if fname not in r.text:
- log.warning('Possibly renamed scene file!\n'
- '\tFilename {}\n\tnot found at {}',
- fname, release_url)
-
-
-def crc(path):
- log.debug('Calculating CRC32 value')
- checksum = 0
- fsize = os.path.getsize(path)
- i = 0
- chunk_size = 4 * 2**20
- with open(path, 'rb') as f:
- with progressbar.DataTransferBar(max_value=fsize,
- max_error=False) as bar:
- while True:
- i += 1
- data = f.read(chunk_size)
- if not data:
- return checksum & 0xFFFFFFFF
-
- bar.update(i*chunk_size)
- checksum = crc32(data, checksum)
-
-
-def is_scene_crc(path):
- checksum = crc(path)
- log.debug('CRC32 {:08X}', checksum)
- r = requests.get(srrdb + 'api/search/archive-crc:%08X' % checksum)
- r.raise_for_status()
-
- scene = int(r.json()['resultsCount']) != 0
- if int(r.json()['resultsCount']) > 1:
- log.warning('More than one srrDB result for CRC32 query')
- log.info('Scene checkbox set to {} '
- 'due to CRC query result'.format(scene))
-
- if scene:
- release = r.json()['results'][0]['release']
- fname = os.path.basename(path)
- check_scene_rename(fname, release)
-
- return scene
-
-
-def query_scene_fname(path):
- if os.path.isfile(path):
- query = os.path.splitext(os.path.basename(path))[0]
- elif os.path.isdir(path):
- query = os.path.basename(path)
- elif not os.path.exists(path):
- raise FileNotFoundError('File or directory not found: %s' % (path,))
+class SceneError(Exception):
+ pass
+
+
+class APIError(Exception):
+ pass
+
+
+# FIXME: "hd1080-wtl.mkv" (from Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080)
+# should report an error, but it's not found on any scene indexers.
+
+
+def check_integrity(path, on_error=None):
+ """
+ Check if `path` (file or directory) was modified
+
+ Files that are not part of a scene release are ignored.
+
+ Return True/False if this release was (not) modified or None if not sure.
+ """
+ errors_found = False
+ something_found = False
+ if _isdir(path):
+ dirname = os.path.basename(path)
else:
- raise Exception('wat')
+ dirname = ''
+ log.debug('Base path for {}: {}', path, dirname)
+
+ guess = _guessit(path)
+ if 'release_group' not in guess:
+ log.debug('No release group found - cannot find scene release: {}', guess)
+ return None
+
+ try:
+ if _isdir(path):
+ try:
+ _check_dir(path)
+ except SceneError as e:
+ errors_found = True
+ something_found = True
+ if on_error is not None:
+ on_error(dirname, e)
+ else:
+ raise
+
+ for info in _yield_file_info(path):
+ log.debug('Scene info: {}', info)
+ # Allow mixed scene/non-scene releases by only checking known scene releases
+ if info['size'] and info['crc']:
+ something_found = True
+ relpath = os.path.join(dirname, info['filename'])
+ if dirname:
+ fspath = os.path.join(path, info['filename'])
+ else:
+ fspath = path
+ log.debug('Torrent path: {}', relpath)
+ log.debug('File system path: {}', fspath)
+ try:
+ _check_file(relpath, fspath, info)
+ except SceneError as e:
+ errors_found = True
+ if on_error is not None:
+ on_error(relpath, e)
+ else:
+ raise
+ except APIError as e:
+ # Connection failed; we can't tell
+ log.debug('Ignoring error: {}', e)
+ if on_error is not None:
+ on_error(dirname or os.path.basename(path), e)
+ else:
+ raise
+ else:
+ log.debug('Something found: {}', something_found)
+ log.debug('Errors found: {}', errors_found)
+ if something_found:
+ return errors_found
+
+
+def _check_dir(path):
+ """
+ Return True if `path` was not modified or None if not sure
- # full search (slow)
- r = requests.get(srrdb + "api/search/{}".format(query))
- r.raise_for_status()
- results = r.json()['results']
+ Raise SceneError if `path` does not match scene release
+ """
+ try:
+ release_name, files = get_details(path)
+ except APIError:
+ return None
+ log.debug('Checking directory: {}', path)
+ log.debug('release_name: {}', release_name)
+ log.debug('files: {}', files)
+ if files:
+ basename = _get_basename_noext(path)
+ log.debug('Base name should be {}: {}', release_name, basename)
+ if release_name != basename:
+ raise SceneError('%s was renamed to %s' % (release_name, basename))
+ else:
+ return True
- if results:
- print('Found srrDB results for filename:')
- print("\t" + "\n".join(r['release'] for r in results))
+
+def _check_file(relpath, fspath, info):
+ """
+ Return True if file was not renamed and has the correct size
+
+ relpath: Relative path that starts with the release name
+ fspath: Path that exists in the file system
+ info: Dictionary from _yield_file_info()
+
+ Raise SceneError if `path` does not match `info`
+ """
+ log.debug('Checking file: {}: {}', relpath, fspath)
+ log.debug('Info: {}', info)
+
+ if info['release_name'] is None:
+ log.debug('No info to check')
+ return None
+
+ release_type = 'file' if relpath == os.path.basename(relpath) else 'directory'
+ log.debug('Release is {}', release_type)
+
+ # File release names are identical to file name sans extension.
+ scene_release_type = 'file' if info['release_name'] == _get_basename_noext(info['filename']) else 'directory'
+ log.debug('Scene release is {}', scene_release_type)
+
+ if scene_release_type == 'file':
+ # Scene released file so we don't care about our own directory name
+ filename = os.path.basename(fspath)
+ exp_filenames = (info['filename'],)
+ log.debug('Existing filename: {}', filename)
+ log.debug('Expected filename: {}', exp_filenames)
+ if filename not in exp_filenames:
+ raise SceneError('%s was renamed to %s' % (exp_filenames[0], filename))
else:
- print('No results found in srrDB for query "{}"'.format(query))
+ # Scene released directory
+ if release_type == 'file':
+ # Don't check directory name
+ filename = os.path.basename(fspath)
+ exp_filenames = (info['filename'],
+ info['release_name'] + '.mkv')
+ else:
+ # Check full internal/relative path
+ filename = os.path.join(os.path.basename(os.path.dirname(fspath)),
+ os.path.basename(fspath))
+ exp_filenames = (os.path.join(info['release_name'], info['filename']),)
+ log.debug('Existing filename: {}', filename)
+ log.debug('Expected filename: {}', exp_filenames)
+ if filename not in exp_filenames:
+ raise SceneError('%s was renamed to %s' % (exp_filenames[0], filename))
+
+ if not _path_exists(fspath):
+ raise SceneError('%s: Missing file' % (fspath,))
+
+ try:
+ filesize = _path_getsize(fspath)
+ except OSError as e:
+ raise SceneError('%s: Unable to get file size: %s' % (fspath, e.strerror))
+ log.debug('File size of {}: {} bytes', fspath, filesize)
+ if filesize != info['size']:
+ raise SceneError('%s: Wrong size: %s instead of %s bytes' %
+ (info['filename'], filesize, info['size']))
+
+ return True
+
+
+def release_names(path):
+ """
+ Get release information from guess it. If we know the title, release group and year or season
+ Search for release title, resolution, release_group and season/episode or year. If the
+ last segment in `path` is not in the results, return them. Otherwise return an empty
+ list.
+ """
+ guess = _guessit(path)
+ # If we don't have enough information, don't bother searching.
+ log.debug('Guess: {}', guess)
+ if 'release_group' not in guess or 'title' not in guess:
+ return []
+
+ log.debug('Looking for release names matching: {}', guess)
+ results = search(guess)
+ if not results and 'season' in guess and 'episode' in guess:
+ # Try again, search for season pack
+ guess.pop('episode', None)
+ log.debug('Looking for release names matching: {}', guess)
+ results = search(guess)
+ return results
+
+
+def search(guess):
+ """
+ Search for scene releases
+
+ guess: Dictionary from guessit.guessit()
+
+ Return a list of release names
+ """
+ log.debug('Searching for {}', guess)
+ # Make search query
+ query = list(guess['title'].split(' '))
+ # Use guess.get(...) to ignore non-existing values as well as None and ""
+ if guess.get('release_group'):
+ query.append('group:' + guess['release_group'])
+ if guess.get('other'):
+ query.append(guess['other'])
+ if guess.get('screen_size'):
+ query.append(guess['screen_size'])
+ if guess.get('type') == 'movie':
+ if guess.get('year'):
+ query.append(guess['year'])
+ elif guess.get('type') == 'episode':
+ episode_info = []
+ if guess.get('season'):
+ episode_info.append('S%02d' % (guess['season'],))
+ if guess.get('episode'):
+ episode_info.append('E%02d' % (guess['episode'],))
+ if episode_info:
+ query.append(''.join(episode_info))
+
+ query_str = '/'.join(str(q) for q in query)
+
+ # Make request
+ try:
+ r = _get("api/search/" + query_str)
+ except APIError:
+ return []
+
+ # Parse response
+ try:
+ json = r.json()
+ except ValueError:
+ log.debug('Invalid JSON: {}', r.text)
+ else:
+ log.debug('Scene search result:\n{}', pprint.pformat(json))
+ if json.get('results'):
+ return [r['release'] for r in json['results']]
+ return []
+
+
+def get_details(path):
+ """
+ Find release info for file or directory at `path`
+
+ Return the release name and a dictionary that maps file names to dictionaries with the
+ keys "release_name", "filename", "size" and "crc".
+ """
+ r = _get('api/details/' + _get_basename_noext(path))
+ # log.debug('Response:\n{}', r.text)
+ try:
+ json = r.json()
+ except ValueError:
+ # The API fixes our release name by capitalizing it correctly, removing file
+ # extension, etc and redirecting us to the releases web page. Try to find the
+ # correct release name in the HTML and make another API request with that.
+ log.debug('Finding real release name in HTML:')
+ soup = BeautifulSoup(r.text, features="html.parser")
+ # log.debug(soup.prettify())
+ release_name_tag = soup.find(id='release-name')
+ if release_name_tag is not None:
+ release_name = release_name_tag.get('value')
+ if release_name:
+ log.debug('Found correct release name: {}', release_name)
+ return get_details(release_name)
+ else:
+ log.debug('Getting info from JSON:')
+ files = json.get('archived-files', [])
+ files_sorted = sorted(files, key=lambda f: f['name'].casefold())
+ return json['name'], {f['name']: {'release_name': json['name'],
+ 'filename': f['name'],
+ 'size': f['size'],
+ 'crc': f['crc']}
+ for f in files_sorted}
+ return '', {}
+
+
+def _yield_file_info(path):
+ """
+ For each file in `path` yield a dictionary with the keys "release_name", "filename",
+ "size" and "crc"
+
+ If no info can be found, all values except for "filename" are `None` and "filename" is
+ taken from `path` instead of the info returned by `get_details`.
+
+ Raise APIError in case of connection or file system error.
+ """
+ # Cover the following cases:
+ # - path is directory (season) and scene released directory
+ # - path is directory and scene released individual file (episodes)
+ # - path is file and scene released individual file
+ # - path is file and scene released directory
+ _, files = get_details(path)
+
+ def default(filepath):
+ return {'release_name': None, 'filename': os.path.basename(filepath), 'size': None, 'crc': None}
+
+ if files:
+ # Scene release is the same as this release (season pack or single episode)
+ yield from files.values()
+
+ elif not _isdir(path):
+ # path is episode and scene release is season pack
+ log.debug('Looking for episode from season pack')
+ guess = _guessit(path)
+ # Try to find whole season if we searched for single episode
+ if 'release_group' in guess and 'season' in guess and 'episode' in guess:
+ guess.pop('episode', None)
+ results = search(guess)
+ # If we find the exact season, pick episode info
+ if len(results) == 1:
+ _, files = get_details(results[0])
+ yield files.get(os.path.basename(path), default(path))
+ else:
+ yield default(path)
+ else:
+ yield default(path)
+
+ else:
+ log.debug('Making season pack from individual episodes')
+ # path is season and scene released single episodes
+ try:
+ files = _os_listdir(path)
+ except OSError as e:
+ raise APIError('%s: %s' % (path, e.strerror))
+ else:
+ log.debug('Found files:', files)
+ for filename in sorted(files):
+ _, files = get_details(filename)
+ yield files.get(filename, default(filename))
+
+
+@functools.lru_cache()
+def _guessit(filepath):
+ release_name = os.path.basename(filepath)
+ if release_name.islower():
+ match = re.search(r'^([a-z0-9]+)-(.+)\.([a-z0-9]{1,3})$', release_name)
+ if match:
+ release_name = '%s-%s.%s' % (match.group(2), match.group(1), match.group(3))
+ return dict(guessit.guessit(release_name))
+
+
+@functools.lru_cache()
+def _get(path):
+ url = srrdb + path
+ log.debug('Requesting URL: {}', url)
+ try:
+ return requests.get(url)
+ except requests.RequestException as e:
+ log.debug('Request failed: {}', e)
+ raise APIError(e)
+
+
+def _get_basename_noext(path):
+ # os.path.splitext() removes anything after the rightmost "." which is no good for
+ # scene release names.
+ return re.sub(r'\.[a-z0-9]{1,3}$$', '', os.path.basename(path))
+
+
+def _isdir(path):
+ # Not using system calls makes testing easier and just looking for file extension
+ # should work pretty much always.
+ return not bool(re.search(r'\.[a-zA-Z0-9]{1,3}$', os.path.basename(path)))
+
+
+# Allow patching in texts without side effects.
+_path_exists = os.path.exists
+_path_getsize = os.path.getsize
+_os_listdir = os.listdir
diff --git a/setup.py b/setup.py
index 8ec411e..e222299 100644
--- a/setup.py
+++ b/setup.py
@@ -41,6 +41,7 @@ def find_version(*file_paths):
"logbook~=1.2",
"pyreadline~=2.1",
"progressbar2~=3.38",
+ "beautifulsoup4~=4.9",
],
python_requires=">=3.5,<4.0",
tests_require=['tox', 'pytest', 'flake8'],
diff --git a/tests/scene_responses/api_details_12 rounds.reloaded.2013.1080p.bluray.x264-rovers.response b/tests/scene_responses/api_details_12 rounds.reloaded.2013.1080p.bluray.x264-rovers.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS.response b/tests/scene_responses/api_details_12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS.response
new file mode 100644
index 0000000..8f3dd5f
--- /dev/null
+++ b/tests/scene_responses/api_details_12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS.response
@@ -0,0 +1 @@
+{"name":"12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS","files":[{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.nfo","size":5400,"crc":"22B0C584"},{"name":"Proof\/12.rounds.reloaded.2013.1080p.bluray.x264-rovers.proof.jpg","size":3810735,"crc":"FEF4344D"},{"name":"Sample\/12.rounds.reloaded.2013.1080p.bluray.x264-rovers.sample.mkv","size":71743141,"crc":"4540D966"},{"name":"Subs\/12.rounds.reloaded.2013.1080p.bluray.x264-rovers.subs.rar","size":4602299,"crc":"D4BB0CC4"},{"name":"Subs\/12.rounds.reloaded.2013.1080p.bluray.x264-rovers.subs.sfv","size":68,"crc":"82AA528E"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.sfv","size":4473,"crc":"82A6BD59"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.rar","size":100000000,"crc":"52E3890D"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r00","size":100000000,"crc":"757269B9"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r01","size":100000000,"crc":"D919BE59"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r02","size":100000000,"crc":"F30146C0"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r03","size":100000000,"crc":"602E8C5B"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r04","size":100000000,"crc":"90C7365F"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r05","size":100000000,"crc":"BE85054C"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r06","size":100000000,"crc":"EB33198B"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r07","size":100000000,"crc":"E4CB58AC"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r08","size":100000000,"crc":"DEDBA13F"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r09","size":100000000,"crc":"9B2A7FD8"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r10","size":100000000,"crc":"6163D203"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r11","size":100000000,"crc":"485049A4"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r12","size":100000000,"crc":"DA8686DD"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r13","size":100000000,"crc":"2068F66E"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r14","size":100000000,"crc":"B7076821"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r15","size":100000000,"crc":"CE92CACF"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r16","size":100000000,"crc":"B66A5439"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r17","size":100000000,"crc":"4C31D7C7"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r18","size":100000000,"crc":"D64FB681"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r19","size":100000000,"crc":"D7CC6CF9"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r20","size":100000000,"crc":"312B1125"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r21","size":100000000,"crc":"D9FA2B12"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r22","size":100000000,"crc":"D5177D64"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r23","size":100000000,"crc":"F713DC2C"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r24","size":100000000,"crc":"2984FAEB"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r25","size":100000000,"crc":"DA143F05"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r26","size":100000000,"crc":"FD12BC74"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r27","size":100000000,"crc":"B49CAE47"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r28","size":100000000,"crc":"7DAB6892"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r29","size":100000000,"crc":"69E149F6"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r30","size":100000000,"crc":"C4069A0F"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r31","size":100000000,"crc":"77D1E410"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r32","size":100000000,"crc":"06192ED4"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r33","size":100000000,"crc":"E18A1302"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r34","size":100000000,"crc":"74C3A397"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r35","size":100000000,"crc":"A03FFF8C"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r36","size":100000000,"crc":"4ECDA98B"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r37","size":100000000,"crc":"78E32A87"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r38","size":100000000,"crc":"082F0074"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r39","size":100000000,"crc":"0C609FE6"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r40","size":100000000,"crc":"C176342D"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r41","size":100000000,"crc":"CB5A543C"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r42","size":100000000,"crc":"60FE8B7E"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r43","size":100000000,"crc":"CD54DEBB"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r44","size":100000000,"crc":"3ABF4FE9"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r45","size":100000000,"crc":"5E71B5B4"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r46","size":100000000,"crc":"CE0C2098"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r47","size":100000000,"crc":"676D6D65"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r48","size":100000000,"crc":"9D525033"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r49","size":100000000,"crc":"481D6B88"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r50","size":100000000,"crc":"21BDE9E1"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r51","size":100000000,"crc":"3CA0ED33"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r52","size":100000000,"crc":"23630486"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r53","size":100000000,"crc":"E7D08129"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r54","size":100000000,"crc":"8F98C957"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r55","size":100000000,"crc":"CEC04178"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r56","size":100000000,"crc":"E6E859E6"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r57","size":100000000,"crc":"9ECA210E"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r58","size":100000000,"crc":"E24CF488"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r59","size":100000000,"crc":"E36B4D89"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r60","size":100000000,"crc":"77C582EB"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r61","size":100000000,"crc":"4DBDB3D0"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r62","size":100000000,"crc":"D2F83E80"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r63","size":100000000,"crc":"09451974"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r64","size":100000000,"crc":"65B4ECA8"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r65","size":100000000,"crc":"38DA416B"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r66","size":100000000,"crc":"C39ADA3F"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r67","size":100000000,"crc":"70FBB9B6"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r68","size":100000000,"crc":"776C13E1"},{"name":"12.rounds.reloaded.2013.1080p.bluray.x264-rovers.r69","size":37070832,"crc":"4F5EA88F"}],"archived-files":[{"name":"12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS.mkv","size":7037061105,"crc":"8EB4C867"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..1d0f250
--- /dev/null
+++ b/tests/scene_responses/api_details_Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT.response
@@ -0,0 +1 @@
+{"name":"Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT","files":[{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.nfo","size":9661,"crc":"5FC023C0"},{"name":"Sample\/bored.to.death.s01.extras.720p.bluray.x264-ingot.sample.mkv","size":45583011,"crc":"7B822E59"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.sfv","size":2016,"crc":"BC37A6D9"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.rar","size":50000000,"crc":"5D2EDF71"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r00","size":50000000,"crc":"BFEC2725"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r01","size":50000000,"crc":"F793EE26"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r02","size":50000000,"crc":"17D8772C"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r03","size":50000000,"crc":"B40E403B"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r04","size":50000000,"crc":"03E5B11D"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r05","size":50000000,"crc":"764F7BA4"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r06","size":50000000,"crc":"B718D156"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r07","size":50000000,"crc":"C7D3E81F"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r08","size":50000000,"crc":"CA6D2A99"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r09","size":50000000,"crc":"65C3EFDB"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r10","size":50000000,"crc":"42BAE872"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r11","size":50000000,"crc":"03E2DBE2"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r12","size":50000000,"crc":"E6C6B0F4"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r13","size":50000000,"crc":"25151B6E"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r14","size":50000000,"crc":"9D7EEEE5"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r15","size":50000000,"crc":"0C1DA53E"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r16","size":50000000,"crc":"BAFD0932"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r17","size":50000000,"crc":"8717D3EE"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r18","size":50000000,"crc":"87A9C45D"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r19","size":50000000,"crc":"E906DBAB"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r20","size":50000000,"crc":"5B717CB2"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r21","size":50000000,"crc":"66667D9B"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r22","size":50000000,"crc":"DE738AFA"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r23","size":50000000,"crc":"9629BB7D"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r24","size":50000000,"crc":"9ABE918C"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r25","size":50000000,"crc":"CA6B3445"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r26","size":50000000,"crc":"D89C4380"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r27","size":50000000,"crc":"D9E1B763"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r28","size":50000000,"crc":"E2D6B514"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r29","size":50000000,"crc":"3F51A592"},{"name":"bored.to.death.s01.extras.720p.bluray.x264-ingot.r30","size":31018792,"crc":"735D3CAE"}],"archived-files":[{"name":"Bored.to.Death.S01.Jonathan.Amess.Brooklyn.720p.BluRay.x264-iNGOT.mkv","size":518652629,"crc":"497277ED"},{"name":"Bored.to.Death.S01.Making.of.720p.BluRay.x264-iNGOT.mkv","size":779447228,"crc":"8C8C0AE7"},{"name":"Bored.to.Death.S01E03.Deleted.Scene.720p.BluRay.x264-iNGOT.mkv","size":30779540,"crc":"0ECBEBE8"},{"name":"Bored.to.Death.S01E04.Deleted.Scene.720p.BluRay.x264-iNGOT.mkv","size":138052914,"crc":"E4E41C29"},{"name":"Bored.to.Death.S01E08.Deleted.Scene.1.720p.BluRay.x264-iNGOT.mkv","size":68498554,"crc":"085C6946"},{"name":"Bored.to.Death.S01E08.Deleted.Scene.2.720p.BluRay.x264-iNGOT.mkv","size":45583011,"crc":"7B822E59"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01.Jonathan.Amess.Brooklyn.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01.Jonathan.Amess.Brooklyn.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01.Making.of.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01.Making.of.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01E03.Deleted.Scene.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01E03.Deleted.Scene.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01E04.Deleted.Scene.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01E04.Deleted.Scene.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01E08.Deleted.Scene.1.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01E08.Deleted.Scene.1.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Bored.to.Death.S01E08.Deleted.Scene.2.720p.BluRay.x264-iNGOT.response b/tests/scene_responses/api_details_Bored.to.Death.S01E08.Deleted.Scene.2.720p.BluRay.x264-iNGOT.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Damnation.S01.720p.AMZN.WEB-DL.DDP5.1.H.264-AJP69.response b/tests/scene_responses/api_details_Damnation.S01.720p.AMZN.WEB-DL.DDP5.1.H.264-AJP69.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Damnation.S01E03.One.Penny.720p.AMZN.WEB-DL.DD+5.1.H.264-AJP69.response b/tests/scene_responses/api_details_Damnation.S01E03.One.Penny.720p.AMZN.WEB-DL.DD+5.1.H.264-AJP69.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..23e78ad
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"2BCB1C7C"},{"name":"Proof\/fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"Sample\/fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.sample.mkv","size":64895080,"crc":"C4AEDC57"},{"name":"Subs\/fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.subs.rar","size":1436125,"crc":"69DC91C0"},{"name":"Subs\/fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"F0EB0D53"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.sfv","size":3008,"crc":"BA225C68"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"FAABAA0E"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"78518B4F"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"6DAE2648"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"885018D7"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"4D96679C"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"BC6A7099"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"D8033B36"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"796180CF"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"0DEF3A3E"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"B91B6E0A"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"30B9DFAC"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"D3C1DC71"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"87EEEB17"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"B72DF7AA"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"98599B96"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"41B41202"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"0A566C30"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"32388121"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"F0CBED55"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"A1AC0873"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"19CA1E6F"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"CF00D162"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"142CF26F"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"7E58CC8E"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"DBC51F24"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"E62772BF"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"E2AFC069"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"655EBCD3"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"19AB9CE0"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"3DACAF2D"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"EF86BA0D"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"3B1D3E24"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"D7B6591C"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"94639D03"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"ACCC1133"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"65C1B0B2"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"30A51990"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"F7C418D9"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"4D451CEF"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"AF7DD283"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"AC8CC3F6"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"60532E1D"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"D96A7C17"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"9A61AB95"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"B2AAF6B7"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"9410CC93"},{"name":"fawlty.towers.s01e01.1080p.bluray.x264-shortbrehd.r45","size":42668734,"crc":"E1E81D07"}],"archived-files":[{"name":"Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.mkv","size":2342662389,"crc":"95042F83"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..c97674e
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"7F289228"},{"name":"Proof\/fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"Sample\/fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.sample.mkv","size":104016055,"crc":"1121F745"},{"name":"Subs\/fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.subs.rar","size":1468376,"crc":"A9F0324C"},{"name":"Subs\/fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"7202B623"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.sfv","size":3008,"crc":"988D4C4E"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"EA877519"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"F18895F5"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"2DA5F9F3"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"1BFC3074"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"0191B7FB"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"924EFE2F"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"D9A34689"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"84681F38"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"EFD83BDB"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"CF701C23"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"5F976555"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"DB002D93"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"CB9A502E"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"9B0FF6FF"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"29BCC161"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"4654CF1B"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"AAD7677C"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"4EC4231D"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"AE47B2B2"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"7D18031A"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"E77569CF"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"728C9864"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"6CA3DDE5"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"FC7B899B"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"49A6C138"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"81369D49"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"FBCFC823"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"DADD3D36"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"29E298D8"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"AEF00A66"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"BA6C2C09"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"23D887FC"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"7A63CE26"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"BA625803"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"295A198F"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"9787DEB1"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"CDB2B19F"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"91066016"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"F959DD37"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"87D7AED4"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"3C8F6E45"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"A93AC42C"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"9754BB75"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"E7A757EC"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"E9617617"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"A39D695C"},{"name":"fawlty.towers.s01e02.1080p.bluray.x264-shortbrehd.r45","size":42812680,"crc":"49F78220"}],"archived-files":[{"name":"Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD.mkv","size":2342806335,"crc":"821D6BF2"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..a514ed0
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"Proof\/fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"959D7AB0"},{"name":"Sample\/fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.sample.mkv","size":116203981,"crc":"13B34A70"},{"name":"Subs\/fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.subs.rar","size":1689149,"crc":"3E83993B"},{"name":"Subs\/fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"E4AEF055"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.sfv","size":3648,"crc":"F566EAAE"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"EF1BC434"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"1468E6A4"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"EEF9AF34"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"35D982F0"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"C50AF12A"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"20A23CD5"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"1E4AA7BF"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"18BF9BAE"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"FAECB584"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"D674F84A"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"62F8E686"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"E8046D4C"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"786F3308"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"0D003810"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"CC5547B8"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"3CEDA0F5"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"5AEA05AF"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"DCC39D4C"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"EBFA4ED8"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"3C32BE6A"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"5228C697"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"0AEB78E8"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"FA0BA4EC"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"278975A6"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"F385C81D"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"29BD00D1"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"6DD0BA5C"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"3067D665"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"E4220524"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"D61838A5"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"2568FBF9"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"FC49F3EE"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"7E8421DC"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"8E166003"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"14ADC7F6"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"F56FBA2D"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"76D2B824"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"0F2E3C85"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"9890DD9B"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"D9D86316"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"61F7079D"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"7ABCA452"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"7383AE2D"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"BE744CE8"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"2A5DA82E"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"AE4D367C"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r45","size":50000000,"crc":"8E81E889"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r46","size":50000000,"crc":"0CA24B6B"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r47","size":50000000,"crc":"6E4811D7"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r48","size":50000000,"crc":"48F2736D"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r49","size":50000000,"crc":"4C3CB5EB"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r50","size":50000000,"crc":"D2A8FE26"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r51","size":50000000,"crc":"FE67C5D4"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r52","size":50000000,"crc":"73FEA061"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r53","size":50000000,"crc":"C3F4691B"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r54","size":50000000,"crc":"F2B04FD6"},{"name":"fawlty.towers.s01e03.1080p.bluray.x264-shortbrehd.r55","size":38660735,"crc":"0772D264"}],"archived-files":[{"name":"Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD.mkv","size":2838653040,"crc":"45B8BD8D"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..9543143
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"C8F30C7D"},{"name":"Proof\/fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"Sample\/fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.sample.mkv","size":89605969,"crc":"A42AC758"},{"name":"Subs\/fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.subs.rar","size":1705346,"crc":"B31AA311"},{"name":"Subs\/fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"5CDE4D66"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.sfv","size":3008,"crc":"7BC0E337"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"988FE795"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"FF17B47B"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"22F47221"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"734CC65F"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"289885A2"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"30AA29A7"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"AF20445A"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"6AB6977D"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"00927923"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"9F8870D4"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"891951B5"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"469BB411"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"A9C68F6D"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"F3B668FC"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"D4E0F508"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"0D0314A4"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"79B5BDC0"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"B7A251FC"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"1813A8A0"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"5F1ECBD1"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"8565F3E6"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"BF63AADB"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"52BFC613"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"D9B77D74"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"568773AB"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"BA038B82"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"199940B8"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"E143A5A9"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"46BCADBB"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"142AB8E8"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"DAE0E633"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"991FEEC4"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"BD058ECA"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"F04BB7EE"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"7E4893B7"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"2D06A56B"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"96CB3800"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"C91808EA"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"D989E69A"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"126D362D"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"DF0FABB2"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"1C19EF70"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"3B468F82"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"BD25FAAE"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"58BE10FF"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"A7B80EDB"},{"name":"fawlty.towers.s01e04.1080p.bluray.x264-shortbrehd.r45","size":43653536,"crc":"F9380291"}],"archived-files":[{"name":"Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD.mkv","size":2343647191,"crc":"8C00A91C"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..65147af
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"FF14629D"},{"name":"Proof\/fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"Sample\/fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.sample.mkv","size":71630278,"crc":"27F9D5C2"},{"name":"Subs\/fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.subs.rar","size":1433304,"crc":"9FB15FEC"},{"name":"Subs\/fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"151958F2"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.sfv","size":3008,"crc":"560D75D1"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"D6A30A99"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"94E30144"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"3A7E94E3"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"EC3FE554"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"1FA3B62F"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"560AD131"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"F174FEE3"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"4310770F"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"44D7009D"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"B43B51D4"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"882E6C7D"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"A6F39F10"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"1205D268"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"96D0407C"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"82962DD9"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"E361AF5B"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"8536B906"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"74294C3B"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"02771934"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"F00926D4"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"E972CB9D"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"512F3E49"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"8D41B4A7"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"38BF3E60"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"058C5455"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"BC341BB8"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"5365F4EA"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"44392B0D"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"14DDFEB9"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"350A5ED7"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"40F6E315"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"9CFCB5FC"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"401D871E"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"3F9D244B"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"1918ECCF"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"C6103CE0"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"3D0BC670"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"3065F9F6"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"0A6CAF2F"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"F5430120"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"E77D90A8"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"84047BDC"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"F128F545"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"E5D57422"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"08679DB7"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"EAF26C91"},{"name":"fawlty.towers.s01e05.1080p.bluray.x264-shortbrehd.r45","size":42964506,"crc":"25E39C43"}],"archived-files":[{"name":"Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD.mkv","size":2342958161,"crc":"C4F1BA16"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD.response b/tests/scene_responses/api_details_Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD.response
new file mode 100644
index 0000000..d43ff3b
--- /dev/null
+++ b/tests/scene_responses/api_details_Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD.response
@@ -0,0 +1 @@
+{"name":"Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD","files":[{"name":"Proof\/fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.proof.jpg","size":132556,"crc":"1B25A106"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.nfo","size":6024,"crc":"08FF1EDF"},{"name":"Sample\/fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.sample.mkv","size":73779293,"crc":"132633F2"},{"name":"Subs\/fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.subs.rar","size":1689863,"crc":"EA1CC8A4"},{"name":"Subs\/fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.subs.sfv","size":69,"crc":"51F44636"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.sfv","size":3008,"crc":"D89BDC9A"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.rar","size":50000000,"crc":"56BE09AA"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r00","size":50000000,"crc":"7C73CDA3"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r01","size":50000000,"crc":"EFE5E877"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r02","size":50000000,"crc":"3129DC4F"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r03","size":50000000,"crc":"BA22CD9E"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r04","size":50000000,"crc":"F0F928F2"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r05","size":50000000,"crc":"266F5149"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r06","size":50000000,"crc":"38426F01"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r07","size":50000000,"crc":"C55B99DE"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r08","size":50000000,"crc":"34F2C1FB"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r09","size":50000000,"crc":"DCF37C32"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r10","size":50000000,"crc":"AE1DA90E"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r11","size":50000000,"crc":"C566729E"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r12","size":50000000,"crc":"C9931C53"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r13","size":50000000,"crc":"FA455B87"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r14","size":50000000,"crc":"51007CBA"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r15","size":50000000,"crc":"EA1E72FC"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r16","size":50000000,"crc":"7992F41A"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r17","size":50000000,"crc":"DF850216"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r18","size":50000000,"crc":"212E6F79"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r19","size":50000000,"crc":"E28A40E0"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r20","size":50000000,"crc":"D6F877A1"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r21","size":50000000,"crc":"0E3B3BAD"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r22","size":50000000,"crc":"E38EC93B"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r23","size":50000000,"crc":"7422D333"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r24","size":50000000,"crc":"4C835128"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r25","size":50000000,"crc":"6E038905"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r26","size":50000000,"crc":"47440B67"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r27","size":50000000,"crc":"B0C6E03F"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r28","size":50000000,"crc":"9BE4CE04"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r29","size":50000000,"crc":"AA803DF9"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r30","size":50000000,"crc":"DB0DB38C"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r31","size":50000000,"crc":"5E7AA746"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r32","size":50000000,"crc":"7664C823"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r33","size":50000000,"crc":"9BC501E6"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r34","size":50000000,"crc":"30B605F9"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r35","size":50000000,"crc":"52E43BE8"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r36","size":50000000,"crc":"4613909E"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r37","size":50000000,"crc":"7E8FC550"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r38","size":50000000,"crc":"72EFD02A"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r39","size":50000000,"crc":"83BB0348"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r40","size":50000000,"crc":"EC5FFAB1"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r41","size":50000000,"crc":"9A0C8E22"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r42","size":50000000,"crc":"2D9E031A"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r43","size":50000000,"crc":"8AA9083F"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r44","size":50000000,"crc":"3707D8B0"},{"name":"fawlty.towers.s01e06.1080p.bluray.x264-shortbrehd.r45","size":42533545,"crc":"C59FFB74"}],"archived-files":[{"name":"Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD.mkv","size":2342527200,"crc":"8AB9CB7C"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_La.Bamba.1987.LE.Bluray.1080p.DTS-HD.x264-Grym.response b/tests/scene_responses/api_details_La.Bamba.1987.LE.Bluray.1080p.DTS-HD.x264-Grym.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Rampart.2011.1080p.Bluray.DD5.1.x264-DON.response b/tests/scene_responses/api_details_Rampart.2011.1080p.Bluray.DD5.1.x264-DON.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_Side.Effects.2013.720p.BluRay.x264-SPARKS.response b/tests/scene_responses/api_details_Side.Effects.2013.720p.BluRay.x264-SPARKS.response
new file mode 100644
index 0000000..3d197fc
--- /dev/null
+++ b/tests/scene_responses/api_details_Side.Effects.2013.720p.BluRay.x264-SPARKS.response
@@ -0,0 +1 @@
+{"name":"Side.Effects.2013.720p.BluRay.x264-SPARKS","files":[{"name":"Sample\/sparks-se2013-720.sample.mkv","size":38590319,"crc":"024774B7"},{"name":"Proof\/sparks-se2013-720-proof.jpg","size":31734,"crc":"752D47B0"},{"name":"sparks-se2013-720.nfo","size":4304,"crc":"44FDACC0"},{"name":"sparks-se2013-720.sfv","size":3008,"crc":"708DD41C"},{"name":"sparks-se2013-720.rar","size":50000000,"crc":"0233504F"},{"name":"sparks-se2013-720.r00","size":50000000,"crc":"0677299D"},{"name":"sparks-se2013-720.r01","size":50000000,"crc":"EC01531C"},{"name":"sparks-se2013-720.r02","size":50000000,"crc":"C044ED66"},{"name":"sparks-se2013-720.r03","size":50000000,"crc":"FA484708"},{"name":"sparks-se2013-720.r04","size":50000000,"crc":"41725205"},{"name":"sparks-se2013-720.r05","size":50000000,"crc":"A9F30BF5"},{"name":"sparks-se2013-720.r06","size":50000000,"crc":"840C9399"},{"name":"sparks-se2013-720.r07","size":50000000,"crc":"2BAC7066"},{"name":"sparks-se2013-720.r08","size":50000000,"crc":"A7F4142A"},{"name":"sparks-se2013-720.r09","size":50000000,"crc":"D0A04760"},{"name":"sparks-se2013-720.r10","size":50000000,"crc":"6BD558B2"},{"name":"sparks-se2013-720.r11","size":50000000,"crc":"6E178796"},{"name":"sparks-se2013-720.r12","size":50000000,"crc":"19E7E43D"},{"name":"sparks-se2013-720.r13","size":50000000,"crc":"FFCC1F83"},{"name":"sparks-se2013-720.r14","size":50000000,"crc":"A970D95B"},{"name":"sparks-se2013-720.r15","size":50000000,"crc":"87F617A5"},{"name":"sparks-se2013-720.r16","size":50000000,"crc":"0A7C2AD4"},{"name":"sparks-se2013-720.r17","size":50000000,"crc":"7277867D"},{"name":"sparks-se2013-720.r18","size":50000000,"crc":"C79F4001"},{"name":"sparks-se2013-720.r19","size":50000000,"crc":"7E6BA438"},{"name":"sparks-se2013-720.r20","size":50000000,"crc":"3671EF14"},{"name":"sparks-se2013-720.r21","size":50000000,"crc":"BBCBC8B5"},{"name":"sparks-se2013-720.r22","size":50000000,"crc":"BCF190DD"},{"name":"sparks-se2013-720.r23","size":50000000,"crc":"27D0767C"},{"name":"sparks-se2013-720.r24","size":50000000,"crc":"B00EE001"},{"name":"sparks-se2013-720.r25","size":50000000,"crc":"073DC106"},{"name":"sparks-se2013-720.r26","size":50000000,"crc":"8B9A046A"},{"name":"sparks-se2013-720.r27","size":50000000,"crc":"D802F557"},{"name":"sparks-se2013-720.r28","size":50000000,"crc":"DCB7BC5B"},{"name":"sparks-se2013-720.r29","size":50000000,"crc":"23D37257"},{"name":"sparks-se2013-720.r30","size":50000000,"crc":"0C664E2C"},{"name":"sparks-se2013-720.r31","size":50000000,"crc":"403648C1"},{"name":"sparks-se2013-720.r32","size":50000000,"crc":"76512350"},{"name":"sparks-se2013-720.r33","size":50000000,"crc":"5352710E"},{"name":"sparks-se2013-720.r34","size":50000000,"crc":"A92EA8C3"},{"name":"sparks-se2013-720.r35","size":50000000,"crc":"9564FE74"},{"name":"sparks-se2013-720.r36","size":50000000,"crc":"6DC41C50"},{"name":"sparks-se2013-720.r37","size":50000000,"crc":"4BD37672"},{"name":"sparks-se2013-720.r38","size":50000000,"crc":"02DEB28D"},{"name":"sparks-se2013-720.r39","size":50000000,"crc":"FAF71B1F"},{"name":"sparks-se2013-720.r40","size":50000000,"crc":"40FAC16E"},{"name":"sparks-se2013-720.r41","size":50000000,"crc":"E65420F9"},{"name":"sparks-se2013-720.r42","size":50000000,"crc":"C51D24B5"},{"name":"sparks-se2013-720.r43","size":50000000,"crc":"616F033A"},{"name":"sparks-se2013-720.r44","size":50000000,"crc":"2AB3A89C"},{"name":"sparks-se2013-720.r45","size":50000000,"crc":"84338566"},{"name":"sparks-se2013-720.r46","size":50000000,"crc":"7EA3F06F"},{"name":"sparks-se2013-720.r47","size":50000000,"crc":"DF40C499"},{"name":"sparks-se2013-720.r48","size":50000000,"crc":"CB54903C"},{"name":"sparks-se2013-720.r49","size":50000000,"crc":"CEE9C201"},{"name":"sparks-se2013-720.r50","size":50000000,"crc":"2BA64EA6"},{"name":"sparks-se2013-720.r51","size":50000000,"crc":"01C346EC"},{"name":"sparks-se2013-720.r52","size":50000000,"crc":"39F39945"},{"name":"sparks-se2013-720.r53","size":50000000,"crc":"F38AC573"},{"name":"sparks-se2013-720.r54","size":50000000,"crc":"F676B007"},{"name":"sparks-se2013-720.r55","size":50000000,"crc":"CA5BCAB7"},{"name":"sparks-se2013-720.r56","size":50000000,"crc":"7B4560FD"},{"name":"sparks-se2013-720.r57","size":50000000,"crc":"46F0BAE7"},{"name":"sparks-se2013-720.r58","size":50000000,"crc":"1306CAA0"},{"name":"sparks-se2013-720.r59","size":50000000,"crc":"874690DC"},{"name":"sparks-se2013-720.r60","size":50000000,"crc":"DD04B043"},{"name":"sparks-se2013-720.r61","size":50000000,"crc":"1549577C"},{"name":"sparks-se2013-720.r62","size":50000000,"crc":"CBB032CE"},{"name":"sparks-se2013-720.r63","size":50000000,"crc":"A59AE9B9"},{"name":"sparks-se2013-720.r64","size":50000000,"crc":"1529019F"},{"name":"sparks-se2013-720.r65","size":50000000,"crc":"0E1AB989"},{"name":"sparks-se2013-720.r66","size":50000000,"crc":"64F9FEFD"},{"name":"sparks-se2013-720.r67","size":50000000,"crc":"355D31A5"},{"name":"sparks-se2013-720.r68","size":50000000,"crc":"D416E0D5"},{"name":"sparks-se2013-720.r69","size":50000000,"crc":"6AB89D1A"},{"name":"sparks-se2013-720.r70","size":50000000,"crc":"58025821"},{"name":"sparks-se2013-720.r71","size":50000000,"crc":"BE9DA1A0"},{"name":"sparks-se2013-720.r72","size":50000000,"crc":"34848609"},{"name":"sparks-se2013-720.r73","size":50000000,"crc":"E4567113"},{"name":"sparks-se2013-720.r74","size":50000000,"crc":"80FD596E"},{"name":"sparks-se2013-720.r75","size":50000000,"crc":"C11432EC"},{"name":"sparks-se2013-720.r76","size":50000000,"crc":"A9026355"},{"name":"sparks-se2013-720.r77","size":50000000,"crc":"3848AA5B"},{"name":"sparks-se2013-720.r78","size":50000000,"crc":"D9E3E607"},{"name":"sparks-se2013-720.r79","size":50000000,"crc":"5107125B"},{"name":"sparks-se2013-720.r80","size":50000000,"crc":"E2CD502C"},{"name":"sparks-se2013-720.r81","size":50000000,"crc":"D90792E8"},{"name":"sparks-se2013-720.r82","size":50000000,"crc":"F04B5266"},{"name":"sparks-se2013-720.r83","size":50000000,"crc":"55AE2CFA"},{"name":"sparks-se2013-720.r84","size":50000000,"crc":"7294E926"},{"name":"sparks-se2013-720.r85","size":50000000,"crc":"62DE8D51"},{"name":"sparks-se2013-720.r86","size":50000000,"crc":"57A38E30"},{"name":"sparks-se2013-720.r87","size":50000000,"crc":"21733A9C"},{"name":"sparks-se2013-720.r88","size":50000000,"crc":"C2F39A6D"},{"name":"sparks-se2013-720.r89","size":50000000,"crc":"7BEC3B11"},{"name":"sparks-se2013-720.r90","size":50000000,"crc":"E71C6F28"},{"name":"sparks-se2013-720.r91","size":50000000,"crc":"6E15D50F"},{"name":"sparks-se2013-720.r92","size":40537059,"crc":"085101CE"}],"archived-files":[{"name":"side.effects.2013.720p.bluray.x264-sparks.mkv","size":4690527189,"crc":"EF39A8F0"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080.response b/tests/scene_responses/api_details_Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080.response
new file mode 100644
index 0000000..ced201f
--- /dev/null
+++ b/tests/scene_responses/api_details_Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080.response
@@ -0,0 +1 @@
+{"name":"Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080","files":[{"name":"hd1080-wtl.nfo","size":421,"crc":"F2BFEEE4"},{"name":"Sample\/hd1080-wtl.sample.mkv","size":76708092,"crc":"2B9D474E"},{"name":"hd1080-wtl.sfv","size":2000,"crc":"10D2EEFB"},{"name":"hd1080-wtl.rar","size":150000000,"crc":"87796979"},{"name":"hd1080-wtl.r00","size":150000000,"crc":"EAEAB676"},{"name":"hd1080-wtl.r01","size":150000000,"crc":"1D072421"},{"name":"hd1080-wtl.r02","size":150000000,"crc":"9604A1E1"},{"name":"hd1080-wtl.r03","size":150000000,"crc":"4508436A"},{"name":"hd1080-wtl.r04","size":150000000,"crc":"487D8751"},{"name":"hd1080-wtl.r05","size":150000000,"crc":"FD8C4E78"},{"name":"hd1080-wtl.r06","size":150000000,"crc":"87E33B7D"},{"name":"hd1080-wtl.r07","size":150000000,"crc":"D0E25933"},{"name":"hd1080-wtl.r08","size":150000000,"crc":"28BBCBB8"},{"name":"hd1080-wtl.r09","size":150000000,"crc":"BC378AE7"},{"name":"hd1080-wtl.r10","size":150000000,"crc":"32A1CD68"},{"name":"hd1080-wtl.r11","size":150000000,"crc":"102F8F06"},{"name":"hd1080-wtl.r12","size":150000000,"crc":"930C1D64"},{"name":"hd1080-wtl.r13","size":150000000,"crc":"4368DBA7"},{"name":"hd1080-wtl.r14","size":150000000,"crc":"2A0734C3"},{"name":"hd1080-wtl.r15","size":150000000,"crc":"33CF1F4B"},{"name":"hd1080-wtl.r16","size":150000000,"crc":"1B783401"},{"name":"hd1080-wtl.r17","size":150000000,"crc":"AEAD3C1A"},{"name":"hd1080-wtl.r18","size":150000000,"crc":"7C241AF5"},{"name":"hd1080-wtl.r19","size":150000000,"crc":"BC527C1E"},{"name":"hd1080-wtl.r20","size":150000000,"crc":"A96B0169"},{"name":"hd1080-wtl.r21","size":150000000,"crc":"AC697BE5"},{"name":"hd1080-wtl.r22","size":150000000,"crc":"760623F1"},{"name":"hd1080-wtl.r23","size":150000000,"crc":"7A05967E"},{"name":"hd1080-wtl.r24","size":150000000,"crc":"3A73211D"},{"name":"hd1080-wtl.r25","size":150000000,"crc":"59390918"},{"name":"hd1080-wtl.r26","size":150000000,"crc":"96148FD7"},{"name":"hd1080-wtl.r27","size":150000000,"crc":"75A5AF4C"},{"name":"hd1080-wtl.r28","size":150000000,"crc":"C32AB57E"},{"name":"hd1080-wtl.r29","size":150000000,"crc":"60FDBE90"},{"name":"hd1080-wtl.r30","size":150000000,"crc":"A1BA7AB6"},{"name":"hd1080-wtl.r31","size":150000000,"crc":"716B3761"},{"name":"hd1080-wtl.r32","size":150000000,"crc":"5288DDA1"},{"name":"hd1080-wtl.r33","size":150000000,"crc":"61BB1C38"},{"name":"hd1080-wtl.r34","size":150000000,"crc":"4CD39909"},{"name":"hd1080-wtl.r35","size":150000000,"crc":"DC8D618A"},{"name":"hd1080-wtl.r36","size":150000000,"crc":"C2EA7D5A"},{"name":"hd1080-wtl.r37","size":150000000,"crc":"805EF795"},{"name":"hd1080-wtl.r38","size":150000000,"crc":"2593591C"},{"name":"hd1080-wtl.r39","size":150000000,"crc":"5E0CD74E"},{"name":"hd1080-wtl.r40","size":150000000,"crc":"D89FD277"},{"name":"hd1080-wtl.r41","size":150000000,"crc":"41E14C60"},{"name":"hd1080-wtl.r42","size":150000000,"crc":"0255C933"},{"name":"hd1080-wtl.r43","size":150000000,"crc":"35E93905"},{"name":"hd1080-wtl.r44","size":150000000,"crc":"3FD72F27"},{"name":"hd1080-wtl.r45","size":150000000,"crc":"E168FC38"},{"name":"hd1080-wtl.r46","size":150000000,"crc":"28C457E0"},{"name":"hd1080-wtl.r47","size":150000000,"crc":"AA91C95F"},{"name":"hd1080-wtl.r48","size":150000000,"crc":"6750E338"},{"name":"hd1080-wtl.r49","size":150000000,"crc":"6D808BE1"},{"name":"hd1080-wtl.r50","size":150000000,"crc":"BB0983AD"},{"name":"hd1080-wtl.r51","size":150000000,"crc":"435C62A0"},{"name":"hd1080-wtl.r52","size":150000000,"crc":"04BF75DC"},{"name":"hd1080-wtl.r53","size":150000000,"crc":"90A1B36A"},{"name":"hd1080-wtl.r54","size":150000000,"crc":"5196D672"},{"name":"hd1080-wtl.r55","size":150000000,"crc":"7412A39F"},{"name":"hd1080-wtl.r56","size":150000000,"crc":"518315DA"},{"name":"hd1080-wtl.r57","size":150000000,"crc":"7907C6A8"},{"name":"hd1080-wtl.r58","size":150000000,"crc":"8D6A9322"},{"name":"hd1080-wtl.r59","size":150000000,"crc":"839A1099"},{"name":"hd1080-wtl.r60","size":150000000,"crc":"BEFA2C9C"},{"name":"hd1080-wtl.r61","size":150000000,"crc":"1D6B95C6"},{"name":"hd1080-wtl.r62","size":150000000,"crc":"5B8D4D2B"},{"name":"hd1080-wtl.r63","size":150000000,"crc":"FC1E166C"},{"name":"hd1080-wtl.r64","size":150000000,"crc":"411BCC7A"},{"name":"hd1080-wtl.r65","size":150000000,"crc":"FE8EB8B0"},{"name":"hd1080-wtl.r66","size":150000000,"crc":"DF25B20D"},{"name":"hd1080-wtl.r67","size":150000000,"crc":"47814BDD"},{"name":"hd1080-wtl.r68","size":150000000,"crc":"028710C8"},{"name":"hd1080-wtl.r69","size":150000000,"crc":"D6273B8C"},{"name":"hd1080-wtl.r70","size":150000000,"crc":"B0B2CF90"},{"name":"hd1080-wtl.r71","size":150000000,"crc":"E0C8B238"},{"name":"hd1080-wtl.r72","size":150000000,"crc":"FF615FCD"},{"name":"hd1080-wtl.r73","size":150000000,"crc":"6D89CB3A"},{"name":"hd1080-wtl.r74","size":150000000,"crc":"20291A5C"},{"name":"hd1080-wtl.r75","size":150000000,"crc":"DCA66420"},{"name":"hd1080-wtl.r76","size":150000000,"crc":"623D622D"},{"name":"hd1080-wtl.r77","size":150000000,"crc":"A2F8CBBA"},{"name":"hd1080-wtl.r78","size":10470851,"crc":"B772CF1F"}],"archived-files":[{"name":"hd1080-wtl.mkv","size":11743374939,"crc":"20C561A5"}],"adds":[]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_details_bored to.death.s01.extras.720p.bluray.x264-ingot.response b/tests/scene_responses/api_details_bored to.death.s01.extras.720p.bluray.x264-ingot.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_bored to.death.s01e03.deleted.scene.720p.bluray.x264-ingot.response b/tests/scene_responses/api_details_bored to.death.s01e03.deleted.scene.720p.bluray.x264-ingot.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_fawlty towers.s01.1080p.bluray.x264-shortbrehd.response b/tests/scene_responses/api_details_fawlty towers.s01.1080p.bluray.x264-shortbrehd.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_fawlty towers.s01e01.1080p.bluray.x264-shortbrehd.response b/tests/scene_responses/api_details_fawlty towers.s01e01.1080p.bluray.x264-shortbrehd.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_hd1080-wtl.response b/tests/scene_responses/api_details_hd1080-wtl.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_side effects.2013.720p.bluray.x264-sparks.response b/tests/scene_responses/api_details_side effects.2013.720p.bluray.x264-sparks.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_side.effects.2013.720p.bluray.x264-sparks.response b/tests/scene_responses/api_details_side.effects.2013.720p.bluray.x264-sparks.response
new file mode 100644
index 0000000..180bfa8
--- /dev/null
+++ b/tests/scene_responses/api_details_side.effects.2013.720p.bluray.x264-sparks.response
@@ -0,0 +1,954 @@
+
+
+
+
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r00.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r01.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r02.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r03.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r04.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r05.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r06.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r07.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r08.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r09.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r10.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r11.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r12.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r13.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r14.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r15.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r16.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r17.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r18.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r19.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r20.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r21.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r22.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r23.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r24.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r25.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r26.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r27.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r28.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r29.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r30.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r31.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r32.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r33.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r34.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r35.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r36.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r37.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r38.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r39.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r40.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r41.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r42.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r43.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r44.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r45.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r46.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r47.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r48.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r49.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r50.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r51.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r52.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r53.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r54.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r55.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r56.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r57.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r58.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r59.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r60.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r61.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r62.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r63.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r64.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r65.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r66.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r67.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r68.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r69.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r70.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r71.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r72.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r73.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r74.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r75.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r76.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r77.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r78.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r79.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r80.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r81.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r82.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r83.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r84.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r85.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r86.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r87.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r88.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r89.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r90.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r91.
+ MHD_FIRSTVOLUME flag set for sparks-se2013-720.r92.
+ Custom RAR packer detected.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/scene_responses/api_details_voa-the_omega_man_x264_bluray.response b/tests/scene_responses/api_details_voa-the_omega_man_x264_bluray.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_details_walk the.line.extended.cut.2005.1080p.bluray.x264-hd1080.response b/tests/scene_responses/api_details_walk the.line.extended.cut.2005.1080p.bluray.x264-hd1080.response
new file mode 100644
index 0000000..e69de29
diff --git a/tests/scene_responses/api_search_12_rounds_reloaded_group:rovers_1080p_2013.response b/tests/scene_responses/api_search_12_rounds_reloaded_group:rovers_1080p_2013.response
new file mode 100644
index 0000000..6103bb8
--- /dev/null
+++ b/tests/scene_responses/api_search_12_rounds_reloaded_group:rovers_1080p_2013.response
@@ -0,0 +1 @@
+{"results":[{"release":"12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS","date":"2013-05-24 13:52:33","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%12%","%rounds%","%reloaded%","rovers","%1080p%","%2013%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_Bored_to_Death_group:iNGOT_720p_S01.response b/tests/scene_responses/api_search_Bored_to_Death_group:iNGOT_720p_S01.response
new file mode 100644
index 0000000..f80beee
--- /dev/null
+++ b/tests/scene_responses/api_search_Bored_to_Death_group:iNGOT_720p_S01.response
@@ -0,0 +1 @@
+{"results":[{"release":"Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT","date":"2011-10-07 00:00:00","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%Bored%","%to%","%Death%","iNGOT","%720p%","%S01%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_Damnation_group:AJP69_720p_S01.response b/tests/scene_responses/api_search_Damnation_group:AJP69_720p_S01.response
new file mode 100644
index 0000000..9ca2d9f
--- /dev/null
+++ b/tests/scene_responses/api_search_Damnation_group:AJP69_720p_S01.response
@@ -0,0 +1 @@
+{"results":[],"resultsCount":"0","warnings":[],"query":["%Damnation%","AJP69","%720p%","%S01%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_La_Bamba_group:Grym_1080p_1987.response b/tests/scene_responses/api_search_La_Bamba_group:Grym_1080p_1987.response
new file mode 100644
index 0000000..0f11e1b
--- /dev/null
+++ b/tests/scene_responses/api_search_La_Bamba_group:Grym_1080p_1987.response
@@ -0,0 +1 @@
+{"results":[],"resultsCount":"0","warnings":[],"query":["%La%","%Bamba%","Grym","%1080p%","%1987%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_Rampart_group:DON_1080p_2011.response b/tests/scene_responses/api_search_Rampart_group:DON_1080p_2011.response
new file mode 100644
index 0000000..c2bfe04
--- /dev/null
+++ b/tests/scene_responses/api_search_Rampart_group:DON_1080p_2011.response
@@ -0,0 +1 @@
+{"results":[],"resultsCount":"0","warnings":[],"query":["%Rampart%","DON","%1080p%","%2011%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_bored_to_death_group:ingot_720p_S01.response b/tests/scene_responses/api_search_bored_to_death_group:ingot_720p_S01.response
new file mode 100644
index 0000000..b9d3714
--- /dev/null
+++ b/tests/scene_responses/api_search_bored_to_death_group:ingot_720p_S01.response
@@ -0,0 +1 @@
+{"results":[{"release":"Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT","date":"2011-10-07 00:00:00","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%bored%","%to%","%death%","ingot","%720p%","%S01%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_bored_to_death_group:ingot_Extras_720p_S01.response b/tests/scene_responses/api_search_bored_to_death_group:ingot_Extras_720p_S01.response
new file mode 100644
index 0000000..01bd19a
--- /dev/null
+++ b/tests/scene_responses/api_search_bored_to_death_group:ingot_Extras_720p_S01.response
@@ -0,0 +1 @@
+{"results":[{"release":"Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT","date":"2011-10-07 00:00:00","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%bored%","%to%","%death%","ingot","%Extras%","%720p%","%S01%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_fawlty_towers_group:shortbrehd_1080p_S01.response b/tests/scene_responses/api_search_fawlty_towers_group:shortbrehd_1080p_S01.response
new file mode 100644
index 0000000..6c72b4e
--- /dev/null
+++ b/tests/scene_responses/api_search_fawlty_towers_group:shortbrehd_1080p_S01.response
@@ -0,0 +1 @@
+{"results":[{"release":"Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:02:14","hasNFO":"yes","hasSRS":"yes"},{"release":"Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:14:50","hasNFO":"yes","hasSRS":"yes"},{"release":"Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:17:02","hasNFO":"yes","hasSRS":"yes"},{"release":"Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:14:14","hasNFO":"yes","hasSRS":"yes"},{"release":"Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:11:40","hasNFO":"yes","hasSRS":"yes"},{"release":"Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD","date":"2019-11-20 13:15:06","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"6","warnings":[],"query":["%fawlty%","%towers%","shortbrehd","%1080p%","%S01%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_side_effects_group:sparks_720p_2013.response b/tests/scene_responses/api_search_side_effects_group:sparks_720p_2013.response
new file mode 100644
index 0000000..28d2de3
--- /dev/null
+++ b/tests/scene_responses/api_search_side_effects_group:sparks_720p_2013.response
@@ -0,0 +1 @@
+{"results":[{"release":"Side.Effects.2013.720p.BluRay.x264-SPARKS","date":"2013-05-08 03:56:18","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%side%","%effects%","sparks","%720p%","%2013%"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_the_omega_man_group:voa.response b/tests/scene_responses/api_search_the_omega_man_group:voa.response
new file mode 100644
index 0000000..4692d3c
--- /dev/null
+++ b/tests/scene_responses/api_search_the_omega_man_group:voa.response
@@ -0,0 +1 @@
+{"results":[{"release":"The.Omega.Man.1971.1080p.BluRay.x264-VOA","date":"2011-12-24 00:00:00","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%the%","%omega%","%man%","voa"]}
\ No newline at end of file
diff --git a/tests/scene_responses/api_search_walk_the_line_group:hd1080_1080p_2005.response b/tests/scene_responses/api_search_walk_the_line_group:hd1080_1080p_2005.response
new file mode 100644
index 0000000..3705970
--- /dev/null
+++ b/tests/scene_responses/api_search_walk_the_line_group:hd1080_1080p_2005.response
@@ -0,0 +1 @@
+{"results":[{"release":"Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080","date":"2012-05-28 00:00:00","hasNFO":"yes","hasSRS":"yes"}],"resultsCount":"1","warnings":[],"query":["%walk%","%the%","%line%","hd1080","%1080p%","%2005%"]}
\ No newline at end of file
diff --git a/tests/test_scene.py b/tests/test_scene.py
new file mode 100644
index 0000000..dad144b
--- /dev/null
+++ b/tests/test_scene.py
@@ -0,0 +1,638 @@
+from pythonbits import scene, bb
+from unittest.mock import patch, Mock, call
+import pytest
+
+import json
+import os
+import contextlib
+
+# Silence guessit
+import logging
+logging.getLogger('rebulk.rebulk').setLevel(logging.INFO)
+logging.getLogger('rebulk.rules').setLevel(logging.INFO)
+logging.getLogger('rebulk.processors').setLevel(logging.INFO)
+
+
+def mock_response(text):
+ return Mock(text=text, json=lambda: json.loads(text))
+
+def mock_html(body):
+ return (''
+ ''
+ '%s'
+ '') % (body,)
+
+
+@patch('pythonbits.scene._yield_file_info')
+@patch('pythonbits.scene._check_dir')
+def test_check_integrity_expects_APIError_from_check_dir(mock_check_dir, mock_yield_file_info):
+ mock_check_dir.side_effect = scene.APIError('Owie!')
+ mock_yield_file_info.return_value = ()
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01.x264-ABC', on_error=cb) is None
+ assert mock_check_dir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_yield_file_info.call_args_list == []
+ assert [repr(arg) for arg in cb.call_args_list] == [repr(call('Foo.S01.x264-ABC', scene.APIError('Owie!')))]
+
+@patch('pythonbits.scene._yield_file_info')
+@patch('pythonbits.scene._check_dir')
+def test_check_integrity_expects_APIError_from_yield_file_info_with_file_release(mock_check_dir, mock_yield_file_info):
+ mock_check_dir.return_value = None
+ mock_yield_file_info.side_effect = scene.APIError('Ooophff!')
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01E02.x264-ABC.mkv', on_error=cb) is None
+ assert mock_check_dir.call_args_list == []
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert [repr(arg) for arg in cb.call_args_list] == [repr(call('Foo.S01E02.x264-ABC.mkv', scene.APIError('Ooophff!')))]
+
+@patch('pythonbits.scene._yield_file_info')
+@patch('pythonbits.scene._check_dir')
+def test_check_integrity_expects_APIError_from_yield_file_info_with_directory_release(mock_check_dir, mock_yield_file_info):
+ mock_check_dir.return_value = None
+ mock_yield_file_info.side_effect = scene.APIError('Ooophff!')
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01.x264-ABC', on_error=cb) is None
+ assert mock_check_dir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert [repr(arg) for arg in cb.call_args_list] == [repr(call('Foo.S01.x264-ABC', scene.APIError('Ooophff!')))]
+
+@patch('pythonbits.scene._yield_file_info')
+@patch('pythonbits.scene._check_dir')
+def test_check_integrity_expects_SceneError_from_check_dir(mock_check_dir, mock_yield_file_info):
+ mock_check_dir.side_effect = scene.SceneError('Nooo!')
+ mock_yield_file_info.return_value = ()
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01.x264-ABC', on_error=cb) is True
+ assert mock_check_dir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert [repr(arg) for arg in cb.call_args_list] == [repr(call('Foo.S01.x264-ABC', scene.SceneError('Nooo!')))]
+
+@patch('pythonbits.scene._check_dir')
+@patch('pythonbits.scene._check_file')
+@patch('pythonbits.scene._yield_file_info')
+def test_check_integrity_expects_SceneError_from_check_file(mock_yield_file_info, mock_check_file, mock_check_dir):
+ mock_check_file.side_effect = scene.SceneError('Argh!')
+ infos = ({'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'},
+ {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'})
+ mock_yield_file_info.return_value = infos
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01.x264-ABC', on_error=cb) is True
+ assert mock_check_dir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_check_file.call_args_list == [
+ call('Foo.S01.x264-ABC/Foo.S01E01.x264-ABC.mkv', 'path/to/Foo.S01.x264-ABC/Foo.S01E01.x264-ABC.mkv', infos[0]),
+ call('Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', infos[1])]
+ assert [repr(arg) for arg in cb.call_args_list] == [repr(call('Foo.S01.x264-ABC/Foo.S01E01.x264-ABC.mkv',
+ scene.SceneError('Argh!'))),
+ repr(call('Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv',
+ scene.SceneError('Argh!')))]
+
+@patch('pythonbits.scene._check_dir')
+@patch('pythonbits.scene._check_file')
+@patch('pythonbits.scene._yield_file_info')
+def test_check_integrity_calls_check_file_correctly_for_directory_release(mock_yield_file_info, mock_check_file, mock_check_dir):
+ infos = ({'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'},
+ {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'})
+ mock_yield_file_info.return_value = infos
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01.x264-ABC', on_error=cb) is False
+ assert mock_check_dir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_check_file.call_args_list == [
+ call('Foo.S01.x264-ABC/Foo.S01E01.x264-ABC.mkv', 'path/to/Foo.S01.x264-ABC/Foo.S01E01.x264-ABC.mkv', infos[0]),
+ call('Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', infos[1])]
+ assert [repr(arg) for arg in cb.call_args_list] == []
+
+@patch('pythonbits.scene._check_dir')
+@patch('pythonbits.scene._check_file')
+@patch('pythonbits.scene._yield_file_info')
+def test_check_integrity_calls_check_file_correctly_for_file_release(mock_yield_file_info, mock_check_file, mock_check_dir):
+ infos = ({'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'},)
+ mock_yield_file_info.return_value = infos
+ cb = Mock()
+ assert scene.check_integrity('path/to/Foo.S01E02.x264-ABC.mkv', on_error=cb) is False
+ assert mock_check_dir.call_args_list == []
+ assert mock_check_file.call_args_list == [call('Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01E02.x264-ABC.mkv', infos[0])]
+ assert mock_yield_file_info.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert [repr(arg) for arg in cb.call_args_list] == []
+
+
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene._guessit')
+def test_release_names_returns_no_results_if_no_release_group(mock_guessit, mock_search):
+ mock_guessit.return_value = {'title': 'The Foo', 'season': 1, 'episode': 2}
+ assert scene.release_names('path/to/The.Foo.S01E02.x264.mkv') == []
+ assert mock_guessit.call_args_list == [call('path/to/The.Foo.S01E02.x264.mkv')]
+ assert mock_search.call_args_list == []
+
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene._guessit')
+def test_release_names_returns_no_results_if_no_title(mock_guessit, mock_search):
+ mock_guessit.return_value = {'release_group': 'ABC', 'season': 1, 'episode': 2}
+ assert scene.release_names('path/to/S01E02.x264-ABC.mkv') == []
+ assert mock_guessit.call_args_list == [call('path/to/S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == []
+
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene._guessit')
+def test_release_names_finds_something_on_first_try(mock_guessit, mock_search):
+ mock_guessit.return_value = {'title': 'The Foo', 'release_group': 'ABC', 'season': 1, 'episode': 2}
+ mock_search.return_value = ['The.Foo.S01E02.x264-ABC.mkv']
+ assert scene.release_names('path/to/The.Foo.S01E02.x264-ABC.mkv') == mock_search.return_value
+ assert mock_guessit.call_args_list == [call('path/to/The.Foo.S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == [call(mock_guessit.return_value)]
+
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene._guessit')
+def test_release_names_finds_looks_for_season_release_on_second_try(mock_guessit, mock_search):
+ mock_guessit.return_value = {'title': 'The Foo', 'release_group': 'ABC', 'season': 1, 'episode': 2}
+ mock_search.side_effect = ([], ['The.Foo.S01.x264-ABC.mkv'])
+ assert scene.release_names('path/to/The.Foo.S01E02.x264-ABC.mkv') == ['The.Foo.S01.x264-ABC.mkv']
+ assert mock_guessit.call_args_list == [call('path/to/The.Foo.S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == [
+ call(mock_guessit.return_value),
+ call({'title': 'The Foo', 'release_group': 'ABC', 'season': 1})]
+
+
+def test_check_file_on_nonscene_file_release():
+ info = {'release_name': None, 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': None, 'crc': None}
+ assert scene._check_file('Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01E02.x264-ABC.mkv', info) is None
+
+def test_check_file_on_nonscene_directory_release():
+ info = {'release_name': None, 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': None, 'crc': None}
+ assert scene._check_file('Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv', info) is None
+
+def test_check_file_reports_wrong_filename_in_file_release_of_original_file_release():
+ info = {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'}
+ msg = r'^Foo.S01E02.x264-ABC.mkv was renamed to Foo S01E02 x264-ABC.mkv$'
+ with pytest.raises(scene.SceneError, match=msg):
+ scene._check_file('Foo S01E02 x264-ABC.mkv', 'path/to/Foo S01E02 x264-ABC.mkv', info)
+
+def test_check_file_reports_wrong_dirname_in_directory_release_of_original_directory_release():
+ info = {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'}
+ msg = r'^Foo.S01.x264-ABC/Foo.S01E02.x264-ABC.mkv was renamed to Foo S01 x264-ABC/Foo.S01E02.x264-ABC.mkv$'
+ with pytest.raises(scene.SceneError, match=msg):
+ scene._check_file('Foo S01 x264-ABC/Foo.S01E02.x264-ABC.mkv', 'path/to/Foo S01 x264-ABC/Foo.S01E02.x264-ABC.mkv', info)
+
+@patch('pythonbits.scene._path_exists', Mock(return_value=True))
+@patch('pythonbits.scene._path_getsize', Mock(return_value=123))
+def test_check_file_ignores_dirname_in_directory_release_of_original_file_release():
+ info = {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'}
+ assert scene._check_file('Foo S01 x264/Foo.S01E02.x264-ABC.mkv', 'path/to/Foo S01 x264/Foo.S01E02.x264-ABC.mkv', info) is True
+
+@patch('pythonbits.scene._path_exists', Mock(return_value=True))
+@patch('pythonbits.scene._path_getsize', Mock(return_value=123))
+def test_check_file_ignores_dirname_in_file_release_of_original_directory_release():
+ info = {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '1234ABCD'}
+ assert scene._check_file('Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01E02.x264-ABC.mkv', info) is True
+
+@patch('pythonbits.scene._path_exists', Mock(return_value=True))
+@patch('pythonbits.scene._path_getsize')
+def test_check_file_finds_wrong_filesize(mock_getsize):
+ mock_getsize.return_value = 124
+ info = {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '12345678'}
+ with pytest.raises(scene.SceneError, match=r'^Foo.S01E02.x264-ABC.mkv: Wrong size: 124 instead of 123 bytes$'):
+ scene._check_file('Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01E02.x264-ABC.mkv', info)
+ assert mock_getsize.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+
+@patch('pythonbits.scene._path_exists', Mock(return_value=True))
+@patch('pythonbits.scene._path_getsize')
+def test_check_file_finds_unmodified_release(mock_getsize):
+ mock_getsize.return_value = 123
+ info = {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 123, 'crc': '12345678'}
+ assert scene._check_file('Foo.S01E02.x264-ABC.mkv', 'path/to/Foo.S01E02.x264-ABC.mkv', info) is True
+ assert mock_getsize.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+
+
+@patch('pythonbits.scene.get_details')
+def test_check_dir_finds_wrong_dirname(mock_get_details):
+ mock_get_details.return_value = (
+ 'Foo.S01.x264-ABC',
+ {'Foo.S01E01.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv', 'size': 123, 'crc': '12345678'},
+ 'Foo.S01E02.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'}})
+ with pytest.raises(scene.SceneError, match=r'^Foo.S01.x264-ABC was renamed to Foo S01 x264-ABC$'):
+ scene._check_dir('path/to/Foo S01 x264-ABC')
+
+@patch('pythonbits.scene.get_details')
+def test_check_dir_finds_correct_dirname(mock_get_details):
+ mock_get_details.return_value = (
+ 'Foo.S01.x264-ABC',
+ {'Foo.S01E01.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv', 'size': 123, 'crc': '12345678'},
+ 'Foo.S01E02.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'}})
+ assert scene._check_dir('path/to/Foo.S01.x264-ABC') is True
+
+@patch('pythonbits.scene.get_details')
+def test_check_dir_fails_to_connect(mock_get_details):
+ mock_get_details.side_effect = scene.APIError()
+ assert scene._check_dir('path/to/Foo.S01.x264-ABC') is None
+
+
+@patch('pythonbits.scene._get')
+def test_get_details_fails_to_connect(mock_get):
+ mock_get.side_effect = scene.APIError('Something went wrong')
+ with pytest.raises(scene.APIError, match='^Something went wrong$'):
+ scene.get_details('path/to/foo.txt')
+ assert mock_get.call_args_list == [call('api/details/foo')]
+
+
+@patch('pythonbits.scene._get')
+def test_get_details_finds_release(mock_get):
+ mock_get.return_value = mock_response(json.dumps({'name': 'Foo',
+ 'archived-files': [{'name': 'Foo.png',
+ 'size': 123,
+ 'crc': '1234ABCD'}]}))
+ assert scene.get_details('path/to/foo.txt') == ('Foo', {'Foo.png': {'release_name': 'Foo',
+ 'filename': 'Foo.png',
+ 'size': 123,
+ 'crc': '1234ABCD'}})
+ assert mock_get.call_args_list == [call('api/details/foo')]
+
+@patch('pythonbits.scene._get')
+def test_get_details_is_redirected_to_correct_web_page(mock_get):
+ mock_get.side_effect = (mock_response(mock_html('')),
+ mock_response(json.dumps({'name': 'Foo',
+ 'archived-files': [{'name': 'Foo.png',
+ 'size': 123,
+ 'crc': '1234ABCD'}]})))
+ assert scene.get_details('foo') == ('Foo', {'Foo.png': {'release_name': 'Foo',
+ 'filename': 'Foo.png',
+ 'size': 123,
+ 'crc': '1234ABCD'}})
+ assert mock_get.call_args_list == [call('api/details/foo'),
+ call('api/details/Foo-Bar')]
+
+@patch('pythonbits.scene._get')
+def test_get_details_finds_nothing(mock_get):
+ mock_get.return_value = mock_response('')
+ assert scene.get_details('foo') == ('', {})
+ assert mock_get.call_args_list == [call('api/details/foo')]
+
+
+@patch('pythonbits.scene._get')
+def test_search_fails_to_connect(mock_get):
+ mock_get.side_effect = scene.APIError()
+ guess = {'title': 'Foo Bar Baz'}
+ assert scene.search(guess) == []
+ assert mock_get.call_args_list == [call('api/search/Foo/Bar/Baz')]
+
+@patch('pythonbits.scene._get')
+def test_search_for_movie(mock_get):
+ mock_get.return_value = mock_response(
+ json.dumps({'results': [{'release': 'The Foo, the Bar and the Bazzy'},
+ {'release': 'Foo Bar goes to Baz'}]}))
+ guess = {'title': 'Foo Bar Baz',
+ 'type': 'movie',
+ 'year': 2005,
+ 'release_group': 'TEHFOO'}
+ assert scene.search(guess) == ['The Foo, the Bar and the Bazzy',
+ 'Foo Bar goes to Baz']
+ assert mock_get.call_args_list == [call('api/search/Foo/Bar/Baz/group:TEHFOO/2005')]
+
+@patch('pythonbits.scene._get')
+def test_search_for_episode(mock_get):
+ mock_get.return_value = mock_response(
+ json.dumps({'results': [{'release': 'The Foo, the Bar and the Bazzy'},
+ {'release': 'Foo Bar goes to Baz'}]}))
+ guess = {'title': 'Foo Bar Baz',
+ 'type': 'episode',
+ 'season': 3, 'episode': 5,
+ 'release_group': 'TEHFOO'}
+ assert scene.search(guess) == ['The Foo, the Bar and the Bazzy',
+ 'Foo Bar goes to Baz']
+ assert mock_get.call_args_list == [call('api/search/Foo/Bar/Baz/group:TEHFOO/S03E05')]
+
+@patch('pythonbits.scene._get')
+def test_search_for_season(mock_get):
+ mock_get.return_value = mock_response(
+ json.dumps({'results': [{'release': 'The Foo, the Bar and the Bazzy'},
+ {'release': 'Foo Bar goes to Baz'}]}))
+ guess = {'title': 'Foo Bar Baz',
+ 'type': 'episode',
+ 'season': 3,
+ 'release_group': 'TEHFOO'}
+ assert scene.search(guess) == ['The Foo, the Bar and the Bazzy',
+ 'Foo Bar goes to Baz']
+ assert mock_get.call_args_list == [call('api/search/Foo/Bar/Baz/group:TEHFOO/S03')]
+
+
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_season_release_of_original_season_release(mock_get_details):
+ mock_get_details.return_value = (
+ 'Foo.S01',
+ {'Foo.S01E01.mkv': {'release_name': 'Foo.S01', 'filename': 'Foo.S01E01.mkv', 'size': 123, 'crc': '12345678'},
+ 'Foo.S01E02.mkv': {'release_name': 'Foo.S01', 'filename': 'Foo.S01E02.mkv', 'size': 234, 'crc': 'ABCD1234'}})
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01'))
+ assert infos == ({'release_name': 'Foo.S01', 'filename': 'Foo.S01E01.mkv', 'size': 123, 'crc': '12345678'},
+ {'release_name': 'Foo.S01', 'filename': 'Foo.S01E02.mkv', 'size': 234, 'crc': 'ABCD1234'})
+
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_episode_release_of_original_episode_release(mock_get_details):
+ mock_get_details.return_value = (
+ 'Foo.S01E01',
+ {'Foo.S01E01.mkv': {'release_name': 'Foo.S01E01', 'filename': 'Foo.S01E01.mkv', 'size': 123, 'crc': '12345678'}})
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01E01.mkv'))
+ assert infos == ({'release_name': 'Foo.S01E01', 'filename': 'Foo.S01E01.mkv', 'size': 123, 'crc': '12345678'},)
+
+@patch('pythonbits.scene._os_listdir')
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_season_release_of_original_episode_release(mock_get_details, mock_listdir):
+ mock_get_details.side_effect = (
+ ('', {}),
+ ('Foo.S01E01.x264-ABC',
+ {'Foo.S01E01.x264-ABC.mkv': {'release_name': 'Foo.S01E01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv',
+ 'size': 123, 'crc': '12345678'}}),
+ ('Foo.S01E02.x264-ABC',
+ {'Foo.S01E02.x264-ABC.mkv': {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv',
+ 'size': 234, 'crc': 'ABCD1234'}}),
+ ('', {}),
+ )
+ mock_listdir.return_value = ['Foo.S01E01.x264-ABC.mkv', 'Foo.S01E02.x264-ABC.mkv', 'unknown file']
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01.x264-ABC'))
+ assert infos == ({'release_name': 'Foo.S01E01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv', 'size': 123, 'crc': '12345678'},
+ {'release_name': 'Foo.S01E02.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'},
+ {'release_name': None, 'filename': 'unknown file', 'size': None, 'crc': None})
+ assert mock_listdir.call_args_list == [call('path/to/Foo.S01.x264-ABC')]
+ assert mock_get_details.call_args_list == [call('path/to/Foo.S01.x264-ABC'),
+ call('Foo.S01E01.x264-ABC.mkv'),
+ call('Foo.S01E02.x264-ABC.mkv'),
+ call('unknown file')]
+
+@patch('pythonbits.scene._guessit')
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_episode_release_of_original_season_release_with_enough_information(
+ mock_get_details, mock_search, mock_guessit):
+ mock_get_details.side_effect = (
+ ('', {}),
+ ('Foo.S01.x264-ABC',
+ {'Foo.S01E01.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E01.x264-ABC.mkv',
+ 'size': 123, 'crc': '12345678'},
+ 'Foo.S01E02.x264-ABC.mkv': {'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv',
+ 'size': 234, 'crc': 'ABCD1234'}})
+ )
+ mock_guessit.return_value = {'release_group': 'TEHFOO', 'season': 1, 'episode': 2}
+ mock_search.return_value = ['Foo.S01.x264-ABC']
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01E02.x264-ABC.mkv'))
+ assert infos == ({'release_name': 'Foo.S01.x264-ABC', 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': 234, 'crc': 'ABCD1234'},)
+ assert mock_get_details.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv'), call('Foo.S01.x264-ABC')]
+ assert mock_guessit.called_with('path/to/Foo.S01E02.x264-ABC.mkv')
+ assert mock_search.call_args_list == [call({'release_group': 'TEHFOO', 'season': 1})]
+
+@patch('pythonbits.scene._guessit')
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_episode_release_of_original_season_release_with_enough_information_no_retry(
+ mock_get_details, mock_search, mock_guessit):
+ mock_get_details.return_value = ('', {})
+ mock_guessit.return_value = {'release_group': 'TEHFOO', 'season': 1}
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01E02.x264-ABC.mkv'))
+ assert infos == ({'release_name': None, 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': None, 'crc': None},)
+ assert mock_get_details.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_guessit.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == []
+
+@patch('pythonbits.scene._guessit')
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_episode_release_of_original_season_release_with_enough_information_no_search_results(
+ mock_get_details, mock_search, mock_guessit):
+ mock_get_details.return_value = ('', {})
+ mock_guessit.return_value = {'release_group': 'TEHFOO', 'season': 1, 'episode': 2}
+ mock_search.return_value = []
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01E02.x264-ABC.mkv'))
+ assert infos == ({'release_name': None, 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': None, 'crc': None},)
+ assert mock_get_details.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_guessit.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == [call({'release_group': 'TEHFOO', 'season': 1})]
+
+@patch('pythonbits.scene._guessit')
+@patch('pythonbits.scene.search')
+@patch('pythonbits.scene.get_details')
+def test_yield_file_info_for_episode_release_of_original_season_release_with_enough_information_no_group(
+ mock_get_details, mock_search, mock_guessit):
+ mock_get_details.return_value = ('', {})
+ mock_guessit.return_value = {'season': 1, 'episode': 2}
+ mock_search.return_value = []
+ infos = tuple(scene._yield_file_info('path/to/Foo.S01E02.x264-ABC.mkv'))
+ assert infos == ({'release_name': None, 'filename': 'Foo.S01E02.x264-ABC.mkv', 'size': None, 'crc': None},)
+ assert mock_get_details.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_guessit.call_args_list == [call('path/to/Foo.S01E02.x264-ABC.mkv')]
+ assert mock_search.call_args_list == []
+
+
+# Integration tests
+
+# The first item is the release name, all following items are file (name, size) tuples. If
+# there is only one item, it's a single-file release. File size of directories is ignored.
+scene_releases = (
+ # Movie in release name directory
+ (('Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080', -1),
+ ('Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080/hd1080-wtl.mkv', 11743374939)),
+
+ # Release name is identical to name of only file + extension
+ (('12.Rounds.Reloaded.2013.1080p.BluRay.x264-ROVERS.mkv', 7037061105),),
+
+ # Release name is identical to name of only file + extension in lower case
+ (('side.effects.2013.720p.bluray.x264-sparks.mkv', 4690527189),),
+
+ # Same as above but in directory with proper release name
+ (('Side.Effects.2013.720p.BluRay.x264-SPARKS', -1),
+ ('Side.Effects.2013.720p.BluRay.x264-SPARKS/side.effects.2013.720p.bluray.x264-sparks.mkv', 4690527189),),
+
+ # Single episode release
+ (('Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.mkv', 2342662389),),
+
+ # Season pack of single episode releases
+ (('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD', -1),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E01.1080p.BluRay.x264-SHORTBREHD.mkv', 2342662389),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E02.1080p.BluRay.x264-SHORTBREHD.mkv', 2342806335),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E03.1080p.BluRay.x264-SHORTBREHD.mkv', 2838653040),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E04.1080p.BluRay.x264-SHORTBREHD.mkv', 2343647191),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E05.1080p.BluRay.x264-SHORTBREHD.mkv', 2342958161),
+ ('Fawlty.Towers.S01.1080p.BluRay.x264-SHORTBREHD/Fawlty.Towers.S01E06.1080p.BluRay.x264-SHORTBREHD.mkv', 2342527200)),
+
+ # Season pack release
+ (('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT', -1),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01.Jonathan.Amess.Brooklyn.720p.BluRay.x264-iNGOT.mkv', 518652629),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01.Making.of.720p.BluRay.x264-iNGOT.mkv', 779447228),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01E03.Deleted.Scene.720p.BluRay.x264-iNGOT.mkv', 30779540),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01E04.Deleted.Scene.720p.BluRay.x264-iNGOT.mkv', 138052914),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01E08.Deleted.Scene.1.720p.BluRay.x264-iNGOT.mkv', 68498554),
+ ('Bored.to.Death.S01.EXTRAS.720p.BluRay.x264-iNGOT/Bored.to.Death.S01E08.Deleted.Scene.2.720p.BluRay.x264-iNGOT.mkv', 45583011)),
+
+ # Single episode from season pack release
+ (('Bored.to.Death.S01E03.Deleted.Scene.720p.BluRay.x264-iNGOT.mkv', 30779540),),
+)
+
+non_scene_releases = (
+ ('Rampart.2011.1080p.Bluray.DD5.1.x264-DON.mkv', 123),
+ ('La.Bamba.1987.LE.Bluray.1080p.DTS-HD.x264-Grym.mkv', 123),
+ ('Damnation.S01.720p.AMZN.WEB-DL.DDP5.1.H.264-AJP69', 123),
+ ('Damnation.S01E03.One.Penny.720p.AMZN.WEB-DL.DD+5.1.H.264-AJP69.mkv', 123),
+ ('The Film Without a Group (1984).mkv', 123),
+)
+
+capture_requests = False
+real_scene_get = scene._get
+capture_dir = os.path.join(os.path.dirname(__file__), 'scene_responses')
+
+def capture_request(path):
+ filepath = os.path.join(capture_dir, path.replace('/', '_') + '.response')
+ if not capture_requests and os.path.exists(filepath):
+ with open(filepath, 'r') as f:
+ return mock_response(f.read())
+ else:
+ response = real_scene_get(path)
+ if not os.path.exists(capture_dir):
+ os.mkdir(capture_dir)
+ with open(filepath, 'w') as f:
+ f.write(response.text)
+ return response
+
+@contextlib.contextmanager
+def mock_files(*filespecs, path_prefix=''):
+ print('Mocking files:')
+ for fs in filespecs:
+ print(fs)
+
+ def mock_exists(path):
+ for path_,size in filespecs:
+ if os.path.join(path_prefix, path_) == path:
+ return True
+ return False
+
+ def mock_getsize(path):
+ for path_,size in filespecs:
+ if os.path.join(path_prefix, path_) == path:
+ return size
+ raise FileNotFoundError(path)
+
+ exists_p = patch('pythonbits.scene._path_exists', mock_exists)
+ filesize_p = patch('pythonbits.scene._path_getsize', mock_getsize)
+ exists_p.start()
+ filesize_p.start()
+ yield
+ exists_p.stop()
+ filesize_p.stop()
+
+@pytest.mark.parametrize('content', scene_releases, ids=(fs[0][0] for fs in scene_releases))
+@patch('pythonbits.scene._get', capture_request)
+@patch('pythonbits.bb.prompt.yesno')
+@patch('pythonbits.scene._os_listdir')
+def test_detection_of_unmodified_scene_release(mock_listdir, mock_prompt_yesno, content):
+ release_name = content[0][0]
+ mock_listdir.return_value = [os.path.basename(fs[0]) for fs in content[1:]]
+ s = bb.VideoSubmission(path='path/to/' + release_name)
+ with mock_files(*content, path_prefix='path/to'):
+ assert s['scene'] is True
+ assert mock_prompt_yesno.call_args_list == []
+
+@pytest.mark.parametrize('content', scene_releases, ids=(fs[0][0] for fs in scene_releases))
+@patch('pythonbits.scene._get', capture_request)
+@patch('sys.exit')
+@patch('pythonbits.bb.prompt.yesno')
+@patch('pythonbits.scene._os_listdir')
+def test_detection_of_scene_release_with_wrong_file_size(mock_listdir, mock_prompt_yesno, mock_exit, content):
+ mock_prompt_yesno.return_value = False
+ mock_exit.return_value = ''
+ mock_listdir.return_value = [os.path.basename(fs[0]) for fs in content[1:]]
+ release_name = content[0][0]
+ modified_content = ((relpath, size - 1) for relpath,size in content)
+ with mock_files(*modified_content, path_prefix='path/to'):
+ # User wants to abort
+ mock_prompt_yesno.side_effect = (True,)
+ assert bb.VideoSubmission(path='path/to/' + release_name)['scene'] == ''
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True)]
+ assert mock_exit.call_args_list == [call(1)]
+ mock_prompt_yesno.reset_mock()
+ mock_exit.reset_mock()
+
+ # User wants to submit anyway but not as scene
+ mock_prompt_yesno.side_effect = (False, False)
+ assert bb.VideoSubmission(path='path/to/' + release_name)['scene'] is False
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True),
+ call('Is this a scene release?', default=False)]
+ assert mock_exit.call_args_list == []
+ mock_prompt_yesno.reset_mock()
+ mock_exit.reset_mock()
+
+ # User wants to submit anyway and insists on scene release
+ mock_prompt_yesno.side_effect = (False, True)
+ assert bb.VideoSubmission(path='path/to/' + release_name)['scene'] is True
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True),
+ call('Is this a scene release?', default=False)]
+ assert mock_exit.call_args_list == []
+
+@pytest.mark.parametrize('content', scene_releases, ids=(fs[0][0] for fs in scene_releases))
+@patch('pythonbits.scene._get', capture_request)
+@patch('sys.exit')
+@patch('pythonbits.bb.prompt.yesno')
+@patch('pythonbits.scene._os_listdir')
+def test_detection_of_scene_release_with_wrong_release_name(mock_listdir, mock_prompt_yesno, mock_exit, content):
+ mock_prompt_yesno.return_value = False
+ mock_exit.return_value = ''
+ mock_listdir.return_value = [os.path.basename(fs[0]) for fs in content[1:]]
+ correct_release_name = content[0][0]
+ wrong_release_name = correct_release_name.lower().replace('.', ' ', 1)
+ with mock_files(*content, path_prefix='path/to'):
+ # User wants to abort
+ mock_prompt_yesno.side_effect = (True,)
+ assert bb.VideoSubmission(path='path/to/' + wrong_release_name)['scene'] == ''
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True)]
+ assert mock_exit.call_args_list == [call(1)]
+ mock_prompt_yesno.reset_mock()
+ mock_exit.reset_mock()
+
+ # User wants to submit anyway but not as scene
+ mock_prompt_yesno.side_effect = (False, False)
+ assert bb.VideoSubmission(path='path/to/' + wrong_release_name)['scene'] is False
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True),
+ call('Is this a scene release?', default=False)]
+ assert mock_exit.call_args_list == []
+ mock_prompt_yesno.reset_mock()
+ mock_exit.reset_mock()
+
+ # User wants to submit anyway and insists on scene release
+ mock_prompt_yesno.side_effect = (False, True)
+ assert bb.VideoSubmission(path='path/to/' + wrong_release_name)['scene'] is True
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True),
+ call('Is this a scene release?', default=False)]
+ assert mock_exit.call_args_list == []
+
+@pytest.mark.parametrize('content', non_scene_releases, ids=(fs[0] for fs in non_scene_releases))
+@patch('pythonbits.scene._get', capture_request)
+@patch('sys.exit')
+@patch('pythonbits.bb.prompt.yesno')
+@patch('pythonbits.scene._os_listdir')
+def test_detection_of_non_scene_release(mock_listdir, mock_prompt_yesno, mock_exit, content):
+ release_name = content[0]
+ s = bb.VideoSubmission(path='path/to/' + release_name)
+ with mock_files(*content, path_prefix='path/to'):
+ assert s['scene'] is False
+ assert mock_prompt_yesno.call_args_list == []
+
+@patch('pythonbits.scene._get', capture_request)
+@patch('sys.exit')
+@patch('pythonbits.bb.prompt.yesno')
+def test_workaround_for_group_in_front(mock_prompt_yesno, mock_exit):
+ mock_prompt_yesno.side_effect = (True,)
+ mock_exit.return_value = ''
+ wrong_release_name = 'voa-the_omega_man_x264_bluray.mkv'
+ content = (wrong_release_name, 123)
+ with mock_files(content, path_prefix='path/to'):
+ assert bb.VideoSubmission(path='path/to/' + wrong_release_name)['scene'] == ''
+ assert mock_prompt_yesno.call_args_list == [call('Abort?', default=True)]
+ assert mock_exit.call_args_list == [call(1)]
+
+@patch('pythonbits.scene._get', capture_request)
+@patch('sys.exit')
+@patch('pythonbits.bb.prompt.yesno')
+def test_renaming_is_ok_if_file_name_is_release_name(mock_prompt_yesno, mock_exit):
+ mock_prompt_yesno.return_value = False
+ mock_exit.return_value = ''
+ acceptable_release_name = 'Walk.the.Line.Extended.Cut.2005.1080p.BluRay.x264-HD1080.mkv'
+ content = (acceptable_release_name, 11743374939)
+ with mock_files(content, path_prefix='path/to'):
+ assert bb.VideoSubmission(path='path/to/' + acceptable_release_name)['scene'] is True
+ assert mock_prompt_yesno.call_args_list == []
+ assert mock_exit.call_args_list == []