├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── docs └── movienamer.gif ├── movienamer ├── __init__.py ├── confirm.py ├── identify.py ├── keywords.py ├── main.py ├── sanitize.py └── tmdb.py ├── requirements.txt ├── setup.py └── tests ├── test_sanitize.py └── test_tmdb.py /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | 9 | [.travis.yml] 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info/ 2 | build/ 3 | dist/ 4 | 5 | env/ 6 | 7 | *.pyc 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | 5 | install: 6 | - "pip install ." 7 | - "pip install -r requirements.txt" 8 | 9 | script: nosetests 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Divij Bindlish (divijbindlish.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include MANIFEST.in 3 | include requirements.txt 4 | include README.md 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # movienamer 2 | 3 | > Command-line utility to properly name movies 4 | 5 | ![Build Status](https://travis-ci.org/divijbindlish/movienamer.svg?branch=master) 6 | 7 | **movienamer** is inspired by the similarly named project 8 | [tvnamer](https://github.com/dbr/tvnamer). 9 | 10 | ## Installation 11 | 12 | ```sh 13 | $ pip install movienamer 14 | ``` 15 | 16 | ## Usage 17 | 18 | ![movienamer](https://raw.githubusercontent.com/divijbindlish/movienamer/master/docs/movienamer.gif) 19 | 20 | ```sh 21 | $ movienamer movie [movie ...] 22 | ``` 23 | 24 | Recursively rename movies present in a directory. 25 | 26 | ```sh 27 | $ movienamer -r ~/Videos/movies 28 | ``` 29 | 30 | ## Contributing 31 | 32 | Take a look at the open 33 | [issues](https://github.com/divijbindlish/movienamer/issues) and submit a PR! 34 | 35 | ## License 36 | 37 | MIT © [Divij Bindlish](http://divijbindlish.com) 38 | -------------------------------------------------------------------------------- /docs/movienamer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/divijbindlish/movienamer/2f525de37a296f63748e1a8f8fbca6eb30de3ebc/docs/movienamer.gif -------------------------------------------------------------------------------- /movienamer/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'Divij Bindlish' 2 | __email__ = 'dvjbndlsh93@gmail.com' 3 | __version__ = '0.1.1' 4 | __license__ = 'MIT' 5 | -------------------------------------------------------------------------------- /movienamer/confirm.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # The number of results to display at one time 4 | N = 5 5 | 6 | 7 | def _confirmation_text_single(result, filename, extension): 8 | prompt = ['Processing file: %s' % (filename + extension)] 9 | 10 | if result['year'] is not None: 11 | prompt.append('Detected movie: %s [%s]' 12 | % (result['title'], result['year'])) 13 | else: 14 | prompt.append('Detected movie: %s' % result['title']) 15 | 16 | prompt = prompt + ['', 'Rename?', '([y]/n/q)', ''] 17 | 18 | return '\n'.join(prompt) 19 | 20 | 21 | def _confirm_single(result, filename, extension): 22 | input_to_action_map = {'y': 'YES', 'n': 'SKIP', 'q': 'QUIT'} 23 | 24 | text = _confirmation_text_single(result, filename, extension) 25 | confirmation = raw_input(text.encode('utf-8')) 26 | if confirmation == '': 27 | confirmation = 'y' 28 | 29 | while confirmation not in input_to_action_map.keys(): 30 | confirmation = raw_input( 31 | '\n'.join(['Rename?', '([y]/n/q)', '']).encode('utf-8')).lower() 32 | if confirmation == '': 33 | confirmation = 'y' 34 | 35 | action = input_to_action_map[confirmation] 36 | return action 37 | 38 | 39 | def _final_line_multiple(length, start): 40 | final_line = 'Enter option (return for default, ' 41 | if length - start > N: 42 | final_line += '\'m\' for more, ' 43 | final_line += '\'s\' to skip, \'q\' to quit):\n' 44 | 45 | return final_line 46 | 47 | 48 | def _combine_multiple_options(results, start): 49 | prompt = [] 50 | 51 | if results[start]['year'] is not None: 52 | prompt.append('%d: %s [%s] (default)' 53 | % (start+1, results[start]['title'], 54 | results[start]['year'])) 55 | else: 56 | prompt.append('%d: %s (default)' % (start+1, results[start]['title'])) 57 | 58 | for i, result in enumerate(results[start+1:start+N]): 59 | if result['year'] is None: 60 | prompt.append('%d: %s' % (start+i+2, result['title'])) 61 | continue 62 | 63 | prompt.append('%d: %s [%s]' 64 | % (start+i+2, result['title'], result['year'])) 65 | 66 | prompt += [ 67 | '', 68 | _final_line_multiple(len(results), start) 69 | ] 70 | 71 | return prompt 72 | 73 | 74 | def _confirmation_text_multiple(results, filename, extension): 75 | prompt = [ 76 | 'Processing file: %s' % (filename + extension), 77 | 'Detected multiple movies:', 78 | '' 79 | ] 80 | 81 | prompt += _combine_multiple_options(results, 0) 82 | return '\n'.join(prompt) 83 | 84 | 85 | def _confirm_multiple(results, start, filename, extension): 86 | if len(results) - start > N: 87 | actions = [str(i) for i in range(1, start+N+1)] + ['s', 'q', 'm'] 88 | else: 89 | actions = [str(i) for i in range(1, len(results)+1)] + ['s', 'q'] 90 | 91 | if start == 0: 92 | text = '\n'.join( 93 | [ 94 | 'Processing file: %s' % (filename + extension), 95 | 'Detected multiple movies:', 96 | '' 97 | ] + _combine_multiple_options(results, 0) 98 | ) 99 | else: 100 | text = '\n'.join(_combine_multiple_options(results, start)) 101 | 102 | confirmation = raw_input(text.encode('utf-8')).lower() 103 | if confirmation == '': 104 | confirmation = str(start+1) 105 | 106 | if confirmation == 'm' and 'm' in actions: 107 | return _confirm_multiple(results, start+N, filename, extension) 108 | 109 | while confirmation not in actions: 110 | confirmation = raw_input( 111 | _final_line_multiple(len(results), start).encode('utf-8')) 112 | if confirmation == '': 113 | confirmation = str(start+1) 114 | 115 | if confirmation == 'm' and 'm' in actions: 116 | return _confirm_multiple(results, start+N, filename, extension) 117 | 118 | if confirmation.isdigit(): 119 | return confirmation 120 | elif confirmation == 's': 121 | return 'SKIP' 122 | elif confirmation == 'q': 123 | return 'QUIT' 124 | else: 125 | raise Exception 126 | 127 | 128 | def confirm(results, filename, extension): 129 | if len(results) == 1: 130 | action = _confirm_single(results[0], filename, extension) 131 | if action == 'YES': 132 | return '0' 133 | return action 134 | 135 | else: 136 | return _confirm_multiple(results, 0, filename, extension) 137 | -------------------------------------------------------------------------------- /movienamer/identify.py: -------------------------------------------------------------------------------- 1 | import os.path as path 2 | import re 3 | 4 | import Levenshtein 5 | 6 | from .sanitize import sanitize 7 | from .tmdb import search 8 | 9 | 10 | def _gather(filename, directory=None, titles={}): 11 | # Sanitize the input filename 12 | name, year = sanitize(filename) 13 | 14 | # Start with a basic search 15 | results = search(name, year) 16 | 17 | if year is not None and len(results) == 0: 18 | # If no results are found when year is present, 19 | # allow a tolerance of 1 in the year 20 | results = search(name, year + 1) 21 | results = results + search(name, year - 1) 22 | 23 | # Try to find a result with zero error and return 24 | zero_distance_results = [] 25 | for i, result in enumerate(results): 26 | distance = Levenshtein.distance( 27 | unicode(re.sub('[^a-zA-Z0-9]', '', name.lower())), 28 | unicode(re.sub('[^a-zA-Z0-9]', '', result['title'].lower())) 29 | ) 30 | 31 | # Update the results with the distance 32 | result['distance'] = distance 33 | results[i]['distance'] = distance 34 | 35 | # Update the results with year 36 | result['with_year'] = (year is not None) 37 | results[i]['with_year'] = (year is not None) 38 | 39 | # Add count field to the result 40 | result['count'] = 1 41 | results[i]['count'] = 1 42 | 43 | if distance == 0: 44 | zero_distance_results.append(result) 45 | 46 | if len(zero_distance_results) > 0: 47 | # Directly return results with zero error 48 | return zero_distance_results 49 | 50 | if year is not None and len(results) > 0: 51 | # Directly return results which were queried with year 52 | return results 53 | 54 | # If neither zero distance results are present nor is the year, 55 | # accumulate results from directory one level up 56 | if directory is not None: 57 | dirname = directory.split('/')[-1] 58 | results_from_directory = _gather(dirname) 59 | 60 | results_to_be_removed = [] 61 | 62 | # Increment count for all duplicate results 63 | for i, r1 in enumerate(results): 64 | for r2 in results_from_directory: 65 | if r1['popularity'] == r2['popularity']: 66 | # Check with popularity since title can be duplicate 67 | results[i]['count'] += r2['count'] 68 | results_from_directory.remove(r2) 69 | break 70 | 71 | results = results + results_from_directory 72 | 73 | return results 74 | 75 | 76 | def identify(filename, directory=None): 77 | if directory == '' or directory == '.' or directory == '..': 78 | directory = None 79 | 80 | results = _gather(filename, directory) 81 | for i, result in enumerate(results): 82 | # Add year to all the results 83 | try: 84 | results[i]['year'] = re.findall( 85 | '[0-9]{4}', result['release_date'])[0] 86 | except TypeError: 87 | results[i]['year'] = None 88 | 89 | if len(results) == 0: 90 | return [] 91 | 92 | max_distance = 1 + max([result['distance'] for result in results]) 93 | return sorted( 94 | results, 95 | key=lambda r: ((r['count'] ** 1.1) * 96 | ((max_distance - r['distance'])) * 97 | ((1 + r['with_year'])) * 98 | ((r['popularity']))), 99 | reverse=True 100 | ) 101 | -------------------------------------------------------------------------------- /movienamer/keywords.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | case_sensitive_keywords = [ 4 | 'LIMITED', 'EXTENDED', 'PROPER', 'xRG', 'E-SuB', 'LiNE', 'DTS', 'ADTRG', 5 | 'HD', 'UNRATED' 6 | ] 7 | 8 | print_keywords = [ 9 | '720p', '1080p', '480p', 'bdrip', 'bd rip', 'bd.rip', 'brrip', 'br rip', 10 | 'br.rip', 'webscr', 'web scr', 'web.scr', 'dvdrip', 'dvd rip', 'dvd.rip', 11 | 'camrip', 'cam rip', 'cam.rip', 'dvdscr', 'dvd scr', 'dvd.scr', 'hdrip', 12 | 'hd rip', 'hd.rip', 'hdcam', 'hd cam', 'hd.cam', 'hddvd', 'hd dvd', 13 | 'hd.dvd', 'bluray', 'blu ray', 'blu.ray', 'hdts', 'hd ts', 'hd.ts', 14 | 'telesync', 'hdtv', 'hd tv', 'hd.tv', 'ts ', 'ts.', ' ts', '.ts', 'tvrip', 15 | 'tv rip', 'tv.rip', 'web-dl', 'r4', 'r5', 'r6', 16 | ] 17 | 18 | case_insensitive_keywords = [ 19 | 'cd1', 'cd 1', 'cd.1', 'cd2', 'cd 2', 'cd.2', 'disc1', 'disc 1', 'disc.1', 20 | 'disc2', 'disc 2', 'disc.2', 'glowgaze com', 'glowgaze.com', 'glowgaze', 21 | 'g2g fm', 'g2g.fm', 'g2g', 'subbed', 'dubbed', 'unrated', 'subs ', 'subs.', 22 | ' subs', '.subs', 'sub ', 'sub.', ' sub', '.sub', 'ntsc', 'axxo', 'fxm', 23 | 'ntsc', 'yify', 'dd5.1', '5.1ch' 24 | ] 25 | 26 | regex_keywords = [ 27 | 'xvid[ \.\-_]?[a-zA-Z0-9\-]*', 'x264[ \-_]?[a-zA-Z0-9\-]*', 28 | 'ac3[ \-_]?[a-zA-Z0-9\-]*', '\[ ?[^\/\]]*\.com ?\]' 29 | ] 30 | 31 | roman_numerals = [ 32 | 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix', 'x', 'xi', 'xii', 33 | 'xiii', 'xiv', 'xv', 'xvi', 'xvii', 'xviii', 'xix', 'xx' 34 | ] 35 | 36 | year_extract_patterns = [re.compile(pattern) for pattern in [ 37 | '^(?P.+?) ?(?P[0-9]{4}?)[^\\/]*$', 38 | '^(?P[0-9]{4}?) ?(?P.+?)$' 39 | ]] 40 | 41 | video_extensions = [ 42 | '.mkv', '.mp4', '.avi', '.flv', '.rmvb', '.wmv', '.mpeg' 43 | ] 44 | 45 | subtitle_extensions = [ 46 | '.srt', '.sub' 47 | ] 48 | -------------------------------------------------------------------------------- /movienamer/main.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import fnmatch 3 | import os 4 | import sys 5 | 6 | from identify import identify 7 | from confirm import confirm 8 | from keywords import subtitle_extensions, video_extensions 9 | 10 | def movienamer(movie): 11 | directory = '/'.join(movie.split('/')[:-1]) 12 | filename, extension = os.path.splitext(os.path.basename(movie)) 13 | 14 | results = identify(filename, directory) 15 | if len(results) == 0: 16 | print 'No results found. Skipping movie file\n' 17 | return False 18 | 19 | action = confirm(results, filename, extension) 20 | 21 | if action == 'SKIP': 22 | print 'Skipping movie file\n' 23 | return False 24 | elif action == 'QUIT': 25 | print 'Exiting movienamer' 26 | sys.exit() 27 | else: 28 | i = int(action) 29 | result = results[i-1] 30 | 31 | if directory == '': 32 | directory = '.' 33 | 34 | dest = (directory + '/' + 35 | result['title'] + 36 | ' [' + result['year'] + ']' + 37 | extension) 38 | 39 | if os.path.isfile(dest): 40 | print 'File already exists: ' + dest 41 | print 'Overwrite?' 42 | final_confirmation = raw_input('([y]/n/q)'.encode('utf-8')).lower() 43 | if final_confirmation == '': 44 | final_confirmation = 'y' 45 | 46 | if final_confirmation not in ['y', 'n', 'q']: 47 | final_confirmation = raw_input( 48 | '([y]/n/q)'.encode('utf-8')).lower() 49 | if final_confirmation == '': 50 | final_confirmation = 'y' 51 | 52 | if final_confirmation == 'n': 53 | print 'Skipping movie file\n' 54 | return False 55 | elif final_confirmation == 'q': 56 | print 'Exiting movienamer' 57 | sys.exit() 58 | 59 | return movie, dest 60 | 61 | 62 | def main(): 63 | parser = argparse.ArgumentParser( 64 | description='Command-line utlity to organize movies.' 65 | ) 66 | 67 | parser.add_argument('movie', nargs='+', help='movie files to rename') 68 | parser.add_argument('-r', '--recursive', action='store_true', 69 | help='recursively rename movies in deirectories') 70 | 71 | args = vars(parser.parse_args()) 72 | 73 | if len(args) == 0: 74 | raise Exception 75 | 76 | movies = [] 77 | errors = [] 78 | 79 | if args['recursive']: 80 | for movie in args['movie']: 81 | if os.path.isfile(movie): 82 | movies.append(movie) 83 | continue 84 | elif not os.path.isdir(movie): 85 | errors.append(movie) 86 | continue 87 | 88 | for root, dirnames, files in os.walk(movie): 89 | for filename in files: 90 | _, extension = os.path.splitext(filename) 91 | if extension in video_extensions: 92 | movies.append(root + '/' + filename) 93 | 94 | else: 95 | for filename in args['movie']: 96 | _, extension = os.path.splitext(filename) 97 | if extension in video_extensions: 98 | movies.append(filename) 99 | else: 100 | errors.append(filename) 101 | 102 | for i, movie in enumerate(movies): 103 | result = movienamer(movie) 104 | if result == False: 105 | errors.append(movie) 106 | else: 107 | os.rename(*result) 108 | print 'Movie succesfully renamed' 109 | if i + 1 < len(movies): 110 | print '' 111 | 112 | if len(errors) > 0: 113 | print 'Unable to rename the following movie files:' 114 | for i, filename in enumerate(errors): 115 | print '%d: %s' % (i+1, filename) 116 | -------------------------------------------------------------------------------- /movienamer/sanitize.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from .keywords import * 4 | 5 | 6 | def _replace_roman_numerals(name): 7 | if name is None or name == '': 8 | raise Exception 9 | 10 | words = name.split(' ') 11 | for i, word in enumerate(words): 12 | if i == 0: 13 | continue 14 | 15 | if i == len(words) - 1 and word == 'i': 16 | words[i] = str(1) 17 | return ' '.join(words) 18 | 19 | if word in roman_numerals: 20 | words[i] = str(roman_numerals.index(word) + 2) 21 | return ' '.join(words) 22 | 23 | return name 24 | 25 | 26 | def _get_year(name): 27 | if name is None or name == '': 28 | raise Exception 29 | 30 | if len(name.split(' ')) == 1: 31 | return (name, None) 32 | 33 | for pattern in year_extract_patterns: 34 | match = re.match(pattern, name) 35 | if match is not None: 36 | year = match.group('year') 37 | if int(year) >= 1900: 38 | return (str(match.group('name')), int(year)) 39 | 40 | return (name, None) 41 | 42 | 43 | def sanitize(name): 44 | if name is None or name == '': 45 | raise Exception 46 | 47 | for keyword in case_sensitive_keywords: 48 | if keyword in name: 49 | name = name.replace(keyword, '') 50 | 51 | name = name.lower() 52 | for keyword in regex_keywords: 53 | name = re.sub(keyword, '', name) 54 | 55 | for keyword in print_keywords: 56 | if keyword in name: 57 | name = name.replace(keyword, '') 58 | 59 | for keyword in case_insensitive_keywords: 60 | if keyword in name: 61 | name = name.replace(keyword, '') 62 | 63 | name = _replace_roman_numerals(name.strip()) 64 | name = re.sub('[\.\-_\[\(\)\]]', ' ', name) 65 | name = re.sub(' +', ' ', name) 66 | name = name.strip() 67 | 68 | return _get_year(name) 69 | -------------------------------------------------------------------------------- /movienamer/tmdb.py: -------------------------------------------------------------------------------- 1 | import json 2 | import urllib 3 | 4 | import requests 5 | 6 | TMDB_API_KEY = '4ec9e70a6068fa052f00fcd4c03b6c46' 7 | TMDB_HOST = 'http://api.themoviedb.org/3' 8 | 9 | 10 | def search(name, year=None): 11 | if name is None or name == '': 12 | raise Exception 13 | 14 | if isinstance(name, unicode): 15 | name = name.encode('utf8') 16 | 17 | endpoint = TMDB_HOST + '/search/movie' 18 | payload = {'api_key': TMDB_API_KEY, 'query': urllib.quote_plus(str(name))} 19 | if year is not None: 20 | payload['year'] = year 21 | 22 | try: 23 | response = requests.get(endpoint, params=payload, timeout=5) 24 | except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): 25 | raise Exception 26 | 27 | try: 28 | result = json.loads(response.text) 29 | return result['results'] 30 | except ValueError: 31 | raise Exception 32 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose==1.3.7 2 | python-Levenshtein==0.12.0 3 | requests==2.7.0 4 | wheel==0.24.0 5 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup 3 | 4 | with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as f: 5 | required = f.read().splitlines() 6 | 7 | with open(os.path.join(os.path.dirname(__file__), 'README.md')) as f: 8 | description = f.read() 9 | 10 | packages = [ 11 | 'movienamer' 12 | ] 13 | 14 | setup( 15 | name='movienamer', 16 | version=__import__('movienamer').__version__, 17 | author='Divij Bindlish', 18 | author_email='dvjbndlsh93@gmail.com', 19 | description='Command-line utility to organize movies', 20 | license='MIT', 21 | long_description=description, 22 | url='https://github.com/divijbindlish/movienamer', 23 | install_requires=required, 24 | packages=packages, 25 | entry_points={ 26 | 'console_scripts': ['movienamer = movienamer.main:main'] 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_sanitize.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import movienamer.sanitize as sanitize 4 | 5 | 6 | class SanitizeTest(unittest.TestCase): 7 | 8 | def test_sanitizer(self): 9 | items = [ 10 | ('2001 - A Beautiful Mind', ('a beautiful mind', 2001)), 11 | ('Inception', ('inception', None)), 12 | ('Closer.2004.brrip.Xvid-VLiS', ('closer', 2004)), 13 | ('Dead.Man.Down.2013.720p.BluRay.x264.YIFY', 14 | ('dead man down', 2013)), 15 | ('Death.Sentence.2007.BluRay.720p.DTS.x264-WiKi', 16 | ('death sentence', 2007)), 17 | ('Heat [1995]-720p-BRRip-x264-StyLishSaLH', 18 | ('heat', 1995)) 19 | ] 20 | 21 | for value, output in items: 22 | self.assertEqual(sanitize.sanitize(value), output) 23 | -------------------------------------------------------------------------------- /tests/test_tmdb.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import movienamer.tmdb as tmdb 4 | 5 | 6 | class TMDbTest(unittest.TestCase): 7 | 8 | def test_search(self): 9 | movies = tmdb.search('inception') 10 | self.assertIsInstance(movies, list) 11 | self.assertGreater(len(movies), 0) 12 | 13 | movie = movies[0] 14 | self.assertIsInstance(movie, dict) 15 | self.assertTrue('title' in movie) 16 | self.assertTrue('original_language' in movie) 17 | self.assertTrue('release_date' in movie) 18 | self.assertTrue('popularity' in movie) 19 | 20 | empty_list = tmdb.search('!@#$%^&*()') 21 | self.assertIsInstance(empty_list, list) 22 | self.assertEqual(len(empty_list), 0) 23 | --------------------------------------------------------------------------------