diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 8e26c6a8..38b33532 100644 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -1,11 +1,14 @@ import time +import traceback -from couchpotato import get_session +from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase +from qcond import QueryCondenser log = CPLog(__name__) @@ -30,15 +33,217 @@ class ShowBase(MediaBase): }) addEvent('show.add', self.add) - addEvent('show.update_info', self.add) + addEvent('show.update_info', self.updateInfo) addEvent('media.search_query', self.query) - def query(self, library, first = True, condense = True, **kwargs): - if library is list or library.get('type') != 'show': + def addView(self, **kwargs): + add_dict = self.add(params = kwargs) + + return { + 'success': True if add_dict else False, + 'show': add_dict, + } + + def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): + if not params: params = {} + + db = get_db() + + # Identifiers + if not params.get('identifiers'): + msg = 'Can\'t add show without at least 1 identifier.' + log.error(msg) + fireEvent('notify.frontend', type = 'show.no_identifier', message = msg) + return False + + info = params.get('info') + if not info or (info and len(info.get('titles', [])) == 0): + info = fireEvent('show.info', merge = True, identifiers = params.get('identifiers')) + + # Set default title + def_title = self.getDefaultTitle(info) + + # Default profile and category + default_profile = {} + if not params.get('profile_id'): + default_profile = fireEvent('profile.default', single = True) + cat_id = params.get('category_id') + + # Add Show + try: + media = { + '_t': 'media', + 'type': 'show', + 'title': def_title, + 'identifiers': info.get('identifiers'), + 'status': status if status else 'active', + 'profile_id': params.get('profile_id', default_profile.get('_id')), + 'category_id': cat_id if cat_id is not None and len(cat_id) > 0 and cat_id != '-1' else None + } + + # TODO: stuff below is mostly a copy of what is done in movie + # Can we make a base function to do this stuff? + + # Remove season info for later use (save separately) + season_info = info.get('seasons', []) + + # Make sure we don't nest in_wanted data + del info['identifiers'] + try: del info['in_wanted'] + except: pass + try: del info['in_library'] + except: pass + try: del info['identifiers'] + except: pass + try: del info['seasons'] + except: pass + + media['info'] = info + + new = False + try: + m = db.run('media', 'identifiers', params.get('identifiers'), with_doc = True)['doc'] + except: + new = True + m = db.insert(media) + + # Update dict to be usable + m.update(media) + + + added = True + do_search = False + search_after = search_after and self.conf('search_on_add', section = 'showsearcher') + onComplete = None + + if new: + if search_after: + onComplete = self.createOnComplete(m['_id']) + search_after = False + elif force_readd: + + # Clean snatched history + for release in db.run('release', 'for_media', m['_id']): + if release.get('status') in ['downloaded', 'snatched', 'done']: + if params.get('ignore_previous', False): + release['status'] = 'ignored' + db.update(release) + else: + fireEvent('release.delete', release['_id'], single = True) + + m['profile_id'] = params.get('profile_id', default_profile.get('id')) + m['category_id'] = media.get('category_id') + m['last_edit'] = int(time.time()) + + do_search = True + db.update(m) + else: + try: del params['info'] + except: pass + log.debug('Show already exists, not updating: %s', params) + added = False + + # Trigger update info + if added and update_after: + # Do full update to get images etc + fireEventAsync('show.update_info', m['_id'], on_complete = onComplete) + + # Remove releases + for rel in db.run('release', 'for_media', m['_id']): + if rel['status'] is 'available': + db.delete(rel) + + movie_dict = db.run('media', 'to_dict', m['_id']) + + if do_search and search_after: + onComplete = self.createOnComplete(m['_id']) + onComplete() + + if added and notify_after: + + if params.get('title'): + message = 'Successfully added "%s" to your wanted list.' % params.get('title', '') + else: + title = getTitle(m) + if title: + message = 'Successfully added "%s" to your wanted list.' % title + else: + message = 'Successfully added to your wanted list.' + fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message) + + return movie_dict + except: + log.error('Failed adding media: %s', traceback.format_exc()) + + # Add Seasons + for season_info in info.get('seasons', []): + + season = fireEvent('show.season.add', media.get('_id'), season_info) + + for episode_info in season_info.get('seasons', []): + + fireEvent('show.episode.add', season.get('_id'), episode_info) + + def updateInfo(self, media_id = None, identifiers = None): + """ + Update movie information inside media['doc']['info'] + + @param media_id: document id + @param identifiers: identifiers from multiple providers + { + 'thetvdb': 123, + 'imdb': 'tt123123', + .. + } + @param extended: update with extended info (parses more info, actors, images from some info providers) + @return: dict, with media + """ + + if self.shuttingDown(): return - titles = [title['title'] for title in library['titles']] + try: + db = get_db() + + if media_id: + media = db.get('id', media_id) + else: + media = db.get('media', identifiers, with_doc = True)['doc'] + + info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) + + # Don't need those here + try: del info['in_wanted'] + except: pass + try: del info['in_library'] + except: pass + + if not info or len(info) == 0: + log.error('Could not update, no show info to work with: %s', media.get('identifier')) + return False + + # Update basic info + media['info'] = info + + # Update image file + image_urls = info.get('images', []) + existing_files = media.get('files', {}) + self.getPoster(image_urls, existing_files) + + db.update(media) + + return media + except: + log.error('Failed update media: %s', traceback.format_exc()) + + return {} + + def query(self, media, first = True, condense = True, **kwargs): + if media.get('type') != 'show': + return + + titles = media['info']['titles'] if condense: # Use QueryCondenser to build a list of optimal search titles @@ -49,50 +254,9 @@ class ShowBase(MediaBase): titles = condensed_titles else: # Fallback to simplifying titles - titles = [simplify(title) for title in titles] + titles = [simplifyString(title) for title in titles] if first: return titles[0] if titles else None return titles - - def addView(self, **kwargs): - add_dict = self.add(params = kwargs) - - return { - 'success': True if add_dict else False, - 'show': add_dict, - } - - def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status = None): - - db = get_db() - - # Add Show - show = { - 'identifiers': { - 'imdb': 'tt1234', - 'thetvdb': 123, - 'tmdb': 123, - 'rage': 123 - }, - 'status': 'active', - 'title': title, - 'description': description, - 'profile_id': profile_id, - 'category_id': category_id, - 'primary_provider': 'thetvdb', - 'absolute_nr': True, - 'info': {} - } - - show_info = fireEvent('show.info', show.get('identifiers')) - - # Add Seasons - for season_info in show_info.get('seasons', []): - - season = fireEvent('show.season.add', show.get('_id'), season_info) - - for episode_info in season_info.get('seasons', []): - - fireEvent('show.episode.add', season.get('_id'), episode_info) diff --git a/couchpotato/core/media/show/_base/static/search.js b/couchpotato/core/media/show/_base/static/search.js index f01d8669..07b988be 100644 --- a/couchpotato/core/media/show/_base/static/search.js +++ b/couchpotato/core/media/show/_base/static/search.js @@ -98,10 +98,8 @@ Block.Search.ShowItem = new Class({ Api.request('show.add', { 'data': { - 'identifier': self.info.id, - 'id': self.info.id, + 'identifiers': self.info.identifiers, 'type': self.info.type, - 'primary_provider': self.info.primary_provider, 'title': self.title_select.get('value'), 'profile_id': self.profile_select.get('value'), 'category_id': self.category_select.get('value') diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index fb5fbdc0..d7110159 100644 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -16,7 +16,55 @@ class Episode(Plugin): addEvent('media.identifier', self.identifier) addEvent('show.episode.add', self.add) - addEvent('show.episode.update_info', self.update) + addEvent('show.episode.update_info', self.updateInfo) + + def add(self, parent_id, update_after = True): + + # Add Season + season = { + '_t': 'media', + 'type': 'episode', + 'nr': 1, + 'identifiers': { + 'imdb': 'tt1234', + 'thetvdb': 123, + 'tmdb': 123, + 'rage': 123 + }, + 'parent': '_id', + 'info': {}, # Returned dict by providers + } + + episode_exists = True or False + + if episode_exists: + pass #update existing + else: + pass # Add Episode + + + # Update library info + if update_after is not False: + handle = fireEventAsync if update_after is 'async' else fireEvent + handle('show.episode.update_info', season.get('_id'), default_title = toUnicode(attrs.get('title', ''))) + + return season + + def updateInfo(self, media_id = None, default_title = '', force = False): + + if self.shuttingDown(): + return + + # Get new info + fireEvent('episode.info', merge = True) + + # Update/create media + + + # Get images + + + return info def query(self, library, first = True, condense = True, include_identifier = True, **kwargs): if library is list or library.get('type') != 'episode': @@ -77,49 +125,3 @@ class Episode(Plugin): identifier['episode'] = tryInt(identifier['episode'], None) return identifier - - def add(self, parent_id, update_after = True): - - # Add Season - season = { - 'nr': 1, - 'identifiers': { - 'imdb': 'tt1234', - 'thetvdb': 123, - 'tmdb': 123, - 'rage': 123 - }, - 'parent': '_id', - 'info': {}, # Returned dict by providers - } - - episode_exists = True or False - - if episode_exists: - pass #update existing - else: - pass # Add Episode - - - # Update library info - if update_after is not False: - handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.episode.update_info', season.get('_id'), default_title = toUnicode(attrs.get('title', ''))) - - return season - - def update_info(self, media_id = None, default_title = '', force = False): - - if self.shuttingDown(): - return - - # Get new info - fireEvent('episode.info', merge = True) - - # Update/create media - - - # Get images - - - return info diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index cf2a0fd0..cd40fd0f 100644 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -1,12 +1,14 @@ from datetime import datetime -import traceback import os +import traceback + +from couchpotato import Env from couchpotato.core.event import addEvent from couchpotato.core.helpers.encoding import simplifyString, toUnicode +from couchpotato.core.helpers.variable import splitString, tryInt, tryFloat from couchpotato.core.logger import CPLog from couchpotato.core.media.show.providers.base import ShowProvider -from couchpotato.environment import Env from tvdb_api import tvdb_exceptions from tvdb_api.tvdb_api import Tvdb @@ -35,7 +37,7 @@ class TheTVDb(ShowProvider): 'banners': True, 'language': 'en', 'cache': os.path.join(Env.get('cache_dir'), 'thetvdb_api'), - } + } self._setup() def _setup(self): @@ -59,11 +61,12 @@ class TheTVDb(ShowProvider): self._setup() search_string = simplifyString(q) - cache_key = 'thetvdb.cache.%s.%s' % (search_string, limit) + cache_key = 'thetvdb.cache.search.%s.%s' % (search_string, limit) results = self.getCache(cache_key) if not results: log.debug('Searching for show: %s', q) + raw = None try: raw = self.tvdb.search(search_string) @@ -76,17 +79,19 @@ class TheTVDb(ShowProvider): try: nr = 0 for show_info in raw: - show = self.tvdb[int(show_info['id'])] - results.append(self._parseShow(show)) + + results.append(self._parseShow(show_info)) nr += 1 if nr == limit: break + log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results]) self.setCache(cache_key, results) return results except (tvdb_exceptions.tvdb_error, IOError), e: - log.error('Failed parsing TheTVDB for "%s": %s', (show, traceback.format_exc())) + log.error('Failed parsing TheTVDB for "%s": %s', (q, traceback.format_exc())) return False + return results def getShow(self, identifier = None): @@ -100,13 +105,20 @@ class TheTVDb(ShowProvider): return show - def getShowInfo(self, identifier = None): - if not identifier: + def getShowInfo(self, identifiers = None): + """ + + @param identifiers: dict with identifiers per provider + @return: Full show info including season and episode info + """ + + if not identifiers or not identifiers.get('thetvdb'): return None - cache_key = 'thetvdb.cache.%s' % identifier - log.debug('Getting showInfo: %s', cache_key) - result = self.getCache(cache_key) or {} + identifier = tryInt(identifiers.get('thetvdb')) + + cache_key = 'thetvdb.cache.show.%s' % identifier + result = self.getCache(cache_key) if result: return result @@ -115,7 +127,7 @@ class TheTVDb(ShowProvider): result = self._parseShow(show) self.setCache(cache_key, result) - return result + return result or {} def getSeasonInfo(self, identifier = None, params = {}): """Either return a list of all seasons or a single season by number. @@ -203,65 +215,33 @@ class TheTVDb(ShowProvider): return result def _parseShow(self, show): - """ - 'actors': u'|Bryan Cranston|Aaron Paul|Dean Norris|RJ Mitte|Betsy Brandt|Anna Gunn|Laura Fraser|Jesse Plemons|Christopher Cousins|Steven Michael Quezada|Jonathan Banks|Giancarlo Esposito|Bob Odenkirk|', - 'added': None, - 'addedby': None, - 'airs_dayofweek': u'Sunday', - 'airs_time': u'9:00 PM', - 'banner': u'http://thetvdb.com/banners/graphical/81189-g13.jpg', - 'contentrating': u'TV-MA', - 'fanart': u'http://thetvdb.com/banners/fanart/original/81189-28.jpg', - 'firstaired': u'2008-01-20', - 'genre': u'|Crime|Drama|Suspense|', - 'id': u'81189', - 'imdb_id': u'tt0903747', - 'language': u'en', - 'lastupdated': u'1376620212', - 'network': u'AMC', - 'networkid': None, - 'overview': u"Walter White, a struggling high school chemistry teacher is diagnosed with advanced lung cancer. He turns to a life of crime, producing and selling methamphetamine accompanied by a former student, Jesse Pinkman with the aim of securing his family's financial future before he dies.", - 'poster': u'http://thetvdb.com/banners/posters/81189-22.jpg', - 'rating': u'9.3', - 'ratingcount': u'473', - 'runtime': u'60', - 'seriesid': u'74713', - 'seriesname': u'Breaking Bad', - 'status': u'Continuing', - 'zap2it_id': u'SH01009396' - """ # # NOTE: show object only allows direct access via # show['id'], not show.get('id') # - - # TODO: Make sure we have a valid show id, not '' or None - #if len (show['id']) is 0: - # return None + def get(name): + return show.get(name) if not hasattr(show, 'search') else show[name] ## Images - poster = show['poster'] or None - backdrop = show['fanart'] or None + poster = get('poster') + backdrop = get('fanart') - genres = [] if show['genre'] is None else show['genre'].strip('|').split('|') - if show['firstaired'] is not None: - try: year = datetime.strptime(show['firstaired'], '%Y-%m-%d').year + genres = splitString(get('genre'), '|') + if get('firstaired') is not None: + try: year = datetime.strptime(get('firstaired'), '%Y-%m-%d').year except: year = None else: year = None - try: - id = int(show['id']) - except: - id = None - show_data = { - 'id': id, + 'identifiers': { + 'thetvdb': tryInt(get('seriesid')), + 'imdb': get('imdb_id'), + 'zap2it': get('zap2it_id'), + }, 'type': 'show', - 'primary_provider': 'thetvdb', - 'titles': [show['seriesname'] or u'', ], - 'original_title': show['seriesname'] or u'', + 'titles': [get('seriesname')], 'images': { 'poster': [poster] if poster else [], 'backdrop': [backdrop] if backdrop else [], @@ -270,37 +250,37 @@ class TheTVDb(ShowProvider): }, 'year': year, 'genres': genres, - 'imdb': show['imdb_id'] or None, - 'zap2it_id': show['zap2it_id'] or None, - 'seriesid': show['seriesid'] or None, - 'network': show['network'] or None, - 'networkid': show['networkid'] or None, - 'airs_dayofweek': show['airs_dayofweek'] or None, - 'airs_time': show['airs_time'] or None, - 'firstaired': show['firstaired'] or None, - 'released': show['firstaired'] or None, - 'runtime': show['runtime'] or None, - 'contentrating': show['contentrating'] or None, - 'rating': show['rating'] or None, - 'ratingcount': show['ratingcount'] or None, - 'actors': show['actors'] or None, - 'lastupdated': show['lastupdated'] or None, - 'status': show['status'] or None, - 'language': show['language'] or None, + 'network': get('network'), + 'plot': get('overview'), + 'networkid': get('networkid'), + 'airs_dayofweek': get('airs_dayofweek'), + 'airs_time': get('airs_time'), + 'firstaired': get('firstaired'), + 'released': get('firstaired'), + 'runtime': get('runtime'), + 'contentrating': get('contentrating'), + 'rating': { + 'thetvdb': [tryFloat(get('rating')), tryInt(get('ratingcount'))], + }, + 'actors': splitString(get('actors'), '|'), + 'lastupdated': get('lastupdated'), + 'status': get('status'), + 'language': get('language'), } show_data = dict((k, v) for k, v in show_data.iteritems() if v) # Add alternative titles - try: - raw = self.tvdb.search(show['seriesname']) - if raw: - for show_info in raw: - if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None): - for alt_name in show_info['aliasnames'].split('|'): - show_data['titles'].append(toUnicode(alt_name)) - except (tvdb_exceptions.tvdb_error, IOError), e: - log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc())) + # try: + # raw = self.tvdb.search(show['seriesname']) + # if raw: + # for show_info in raw: + # print show_info + # if show_info['id'] == show_data['id'] and show_info.get('aliasnames', None): + # for alt_name in show_info['aliasnames'].split('|'): + # show_data['titles'].append(toUnicode(alt_name)) + # except (tvdb_exceptions.tvdb_error, IOError), e: + # log.error('Failed searching TheTVDB for "%s": %s', (show['seriesname'], traceback.format_exc())) return show_data diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index 2d32e254..c7aca946 100644 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -18,6 +18,55 @@ class Season(Plugin): addEvent('show.season.add', self.update) addEvent('show.season.update_info', self.update) + def add(self, parent_id, update_after = True): + + # Add Season + season = { + '_t': 'media', + 'type': 'season', + 'nr': 1, + 'identifiers': { + 'imdb': 'tt1234', + 'thetvdb': 123, + 'tmdb': 123, + 'rage': 123 + }, + 'parent': '_id', + 'info': {}, # Returned dict by providers + } + + # Check if season already exists + season_exists = True or False + + if season_exists: + pass #update existing + else: + + db.insert(season) + + + # Update library info + if update_after is not False: + handle = fireEventAsync if update_after is 'async' else fireEvent + handle('show.season.update_info', episode.get('_id')) + + return season + + def update_info(self, media_id = None, default_title = '', force = False): + + if self.shuttingDown(): + return + + # Get new info + fireEvent('season.info', merge = True) + + # Update/create media + + # Get images + + + return info + def query(self, library, first = True, condense = True, include_identifier = True, **kwargs): if library is list or library.get('type') != 'season': return @@ -65,50 +114,3 @@ class Season(Plugin): return { 'season': tryInt(library['season_number'], None) } - - def add(self, parent_id, update_after = True): - - # Add Season - season = { - 'nr': 1, - 'identifiers': { - 'imdb': 'tt1234', - 'thetvdb': 123, - 'tmdb': 123, - 'rage': 123 - }, - 'parent': '_id', - 'info': {}, # Returned dict by providers - } - - # Check if season already exists - season_exists = True or False - - if season_exists: - pass #update existing - else: - - db.insert(season) - - - # Update library info - if update_after is not False: - handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update_info', episode.get('_id')) - - return season - - def update_info(self, media_id = None, default_title = '', force = False): - - if self.shuttingDown(): - return - - # Get new info - fireEvent('season.info', merge = True) - - # Update/create media - - # Get images - - - return info