Source code for flexget.components.imdb.from_imdb

from collections.abc import Iterable

from loguru import logger

from flexget import plugin
from flexget.config_schema import one_or_more
from flexget.entry import Entry
from flexget.event import event
from flexget.utils.cached_input import cached

logger = logger.bind(name='from_imdb')


[docs] class FromIMDB: """Enable generating entries based on an entity, an entity being a person, character or company. It's based on IMDBpy which is required (pip install imdbpy). The basic config required just an IMDB ID of the required entity. For example:: from_imdb: ch0001354 Schema description: Other than ID, all other properties are meant to filter the full list that the entity generates. ============== ============================================================================================== Option Description ============== ============================================================================================== id string that relates to a supported entity type. For example: 'nm0000375'. Required. job_types a string or list with job types from job_types. Default is 'actor'. content_types A string or list with content types from content_types. Default is 'movie'. max_entries The maximum number of entries that can return. This value's purpose is basically flood protection against unruly configurations that will return too many results. Default is 200. ============== ============================================================================================== Advanced config example:: dynamic_movie_queue: from_imdb: id: co0051941 job_types: - actor - director content_types: tv series accept_all: yes movie_queue: add """ job_types = [ 'actor', 'actress', 'director', 'producer', 'writer', 'self', 'editor', 'miscellaneous', 'editorial department', 'cinematographer', 'visual effects', 'thanks', 'music department', 'in development', 'archive footage', 'soundtrack', ] content_types = [ 'movie', 'tv series', 'tv mini series', 'video game', 'video movie', 'tv movie', 'episode', ] content_type_conversion = { 'movie': 'movie', 'tv series': 'tv', 'tv mini series': 'tv', 'tv movie': 'tv', 'episode': 'tv', 'video movie': 'video', 'video game': 'video game', } character_content_type_conversion = { 'movie': 'feature', 'tv series': 'tv', 'tv mini series': 'tv', 'tv movie': 'tv', 'episode': 'tv', 'video movie': 'video', 'video game': 'video-game', } jobs_without_content_type = ['actor', 'actress', 'self', 'in development', 'archive footage'] imdb_pattern = one_or_more( { 'type': 'string', 'pattern': r'(nm|co|ch)\d{7,8}', 'error_pattern': 'Get the id from the url of the person/company you want to use,' ' e.g. http://imdb.com/text/<id here>/blah', }, unique_items=True, ) schema = { 'oneOf': [ imdb_pattern, { 'type': 'object', 'properties': { 'id': imdb_pattern, 'job_types': one_or_more( {'type': 'string', 'enum': job_types}, unique_items=True ), 'content_types': one_or_more( {'type': 'string', 'enum': content_types}, unique_items=True ), 'max_entries': {'type': 'integer'}, 'match_type': {'type': 'string', 'enum': ['strict', 'loose']}, }, 'required': ['id'], 'additionalProperties': False, }, ] }
[docs] def prepare_config(self, config): """Convert config to dict form and sets defaults if needed.""" if isinstance(config, str): config = {'id': [config]} elif isinstance(config, list): config = {'id': config} if isinstance(config, dict) and not isinstance(config['id'], list): config['id'] = [config['id']] config.setdefault('content_types', [self.content_types[0]]) config.setdefault('job_types', [self.job_types[0]]) config.setdefault('max_entries', 200) config.setdefault('match_type', 'strict') if isinstance(config.get('content_types'), str): logger.debug('Converted content type from string to list.') config['content_types'] = [config['content_types']] if isinstance(config['job_types'], str): logger.debug('Converted job type from string to list.') config['job_types'] = [config['job_types']] # Special case in case user meant to add actress instead of actor (different job types in IMDB) if 'actor' in config['job_types'] and 'actress' not in config['job_types']: config['job_types'].append('actress') return config
[docs] def get_items(self, config): items = [] for id in config['id']: try: entity_type, entity_object = self.get_entity_type_and_object(id) except Exception as e: logger.error( 'Could not resolve entity via ID: {}. ' 'Either error in config or unsupported entity. Error:{}', id, e, ) continue items += self.get_items_by_entity( entity_type, entity_object, config.get('content_types'), config.get('job_types'), config.get('match_type'), ) return set(items)
[docs] def get_entity_type_and_object(self, imdb_id): """Return a tuple of entity type and entity object. :param imdb_id: string which contains IMDB id :return: entity type, entity object (person, company, etc.) """ if imdb_id.startswith('nm'): person = self.ia.get_person(imdb_id[2:]) logger.info('Starting to retrieve items for person: {}', person) return 'Person', person if imdb_id.startswith('co'): company = self.ia.get_company(imdb_id[2:]) logger.info('Starting to retrieve items for company: {}', company) return 'Company', company if imdb_id.startswith('ch'): character = self.ia.get_character(imdb_id[2:]) logger.info('Starting to retrieve items for Character: {}', character) return 'Character', character return None
[docs] def get_items_by_entity( self, entity_type, entity_object, content_types, job_types, match_type ): """Get entity object and return movie list using relevant method.""" if entity_type == 'Company': return self.items_by_company(entity_object) if entity_type == 'Character': return self.items_by_character(entity_object, content_types, match_type) if entity_type == 'Person': return self.items_by_person(entity_object, job_types, content_types, match_type) return None
[docs] def flatten_list(self, _list): """Get a list of lists and return a flat list.""" for el in _list: if isinstance(el, Iterable) and not isinstance(el, str): yield from self.flatten_list(el) else: yield el
[docs] def flat_list(self, non_flat_list, remove_none=False): flat_list = self.flatten_list(non_flat_list) if remove_none: flat_list = [_f for _f in flat_list if _f] return flat_list
[docs] def filtered_items(self, unfiltered_items, content_types, match_type): items = [] unfiltered_items = set(unfiltered_items) for item in sorted(unfiltered_items): if match_type == 'strict': logger.debug( 'Match type is strict, verifying item type to requested content types' ) self.ia.update(item) if item['kind'] in content_types: logger.verbose( 'Adding item "{}" to list. Item kind is "{}"', item, item['kind'] ) items.append(item) else: logger.verbose('Rejecting item "{}". Item kind is "{}', item, item['kind']) else: logger.debug('Match type is loose, all items are being added') items.append(item) return items
[docs] def items_by_person(self, person, job_types, content_types, match_type): """Return item list for a person object.""" unfiltered_items = self.flat_list( [self.items_by_job_type(person, job_type, content_types) for job_type in job_types], remove_none=True, ) return self.filtered_items(unfiltered_items, content_types, match_type)
[docs] def items_by_content_type(self, person, job_type, content_type): return [ _f for _f in (person.get(job_type + ' ' + self.content_type_conversion[content_type], [])) if _f ]
[docs] def items_by_job_type(self, person, job_type, content_types): items = ( person.get(job_type, []) if job_type in self.jobs_without_content_type else [ ( person.get(job_type + ' ' + 'documentary', []) and person.get(job_type + ' ' + 'short', []) and self.items_by_content_type(person, job_type, content_type) if content_type == 'movie' else self.items_by_content_type(person, job_type, content_type) ) for content_type in content_types ] ) return [_f for _f in items if _f]
[docs] def items_by_character(self, character, content_types, match_type): """Return items list for a character object. :param character: character object :param content_types: content types as defined in config :return: """ unfiltered_items = self.flat_list( [ character.get(self.character_content_type_conversion[content_type]) for content_type in content_types ], remove_none=True, ) return self.filtered_items(unfiltered_items, content_types, match_type)
[docs] def items_by_company(self, company): """Return items list for a company object. :param company: company object :return: company items list """ return company.get('production companies')
@cached('from_imdb', persist='2 hours') def on_task_input(self, task, config): try: from imdb import IMDb self.ia = IMDb() except ImportError: logger.error( 'IMDBPY is required for this plugin. Please install using "pip install imdbpy"' ) return None entries = [] config = self.prepare_config(config) items = self.get_items(config) if not items: logger.error('Could not get IMDB item list, check your configuration.') return None for item in items: entry = Entry( title=item['title'], imdb_id='tt' + self.ia.get_imdbID(item), url='', imdb_url=self.ia.get_imdbURL(item), ) if entry.isvalid(): if entry not in entries: entries.append(entry) if entry and task.options.test: logger.info('Test mode. Entry includes:') for key, value in list(entry.items()): logger.info(' {}: {}', key.capitalize(), value) else: logger.error('Invalid entry created? {}', entry) if len(entries) <= config.get('max_entries'): return entries logger.warning( 'Number of entries ({}) exceeds maximum allowed value {}. ' 'Edit your filters or raise the maximum value by entering a higher "max_entries"', len(entries), config.get('max_entries'), ) return None
[docs] @event('plugin.register') def register_plugin(): plugin.register(FromIMDB, 'from_imdb', api_ver=2)