diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 341f03d1..b1cbe66e 100644 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -82,16 +82,6 @@ class ShowBase(MediaBase): def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): """ - 1. Add Show - 2. Add All Episodes - 3. Add All Seasons - - Notes, not to forget: - - relate parent and children, possible grandparent to grandchild so episodes know it belong to show, etc - - looks like we dont send info to library; it comes later - - change references to plot to description - - change Model to Media - params {'category_id': u'-1', 'identifier': u'tt1519931', @@ -101,36 +91,92 @@ class ShowBase(MediaBase): """ log.debug("show.add") - # Add show parent to db first - parent = self.addToDatabase(params = params, type = 'show') + # Add show parent to db first; need to update library so maps will be in place (if any) + parent = self.addToDatabase(params = params, update_library = True, type = 'show') + # TODO: add by airdate + + # Add by Season/Episode numbers + self.addBySeasonEpisode(parent, + params = params, + force_readd = force_readd, + search_after = search_after, + update_library = update_library, + status_id = status_id + ) + + def addBySeasonEpisode(self, parent, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): identifier = params.get('id') + # 'tvdb' will always be the master for our purpose. All mapped data can be mapped + # to another source for downloading, but it will always be remapped back to tvdb numbering + # when renamed so media can be used in media players that use tvdb for info provider + # + # This currently means the episode must actually exist in tvdb in order to be found but + # the numbering can be different - # XXX: Fix so we dont have a nested list [0] (fireEvent) - try: - seasons = fireEvent('season.info', identifier = identifier)[0] - except: return None + #master = 'tvdb' + #destination = 'scene' + #destination = 'anidb' + #destination = 'rage' + #destination = 'trakt' + # TODO: auto mode. if anime exists use it. if scene exists use it else use tvdb + + # XXX: We should abort adding show, etc if either tvdb or xem is down or we will have incorrent mappings + # I think if tvdb gets error we wont have anydata anyway, but we must make sure XEM returns!!!! + + # Only the master should return results here; all other info providers should just return False + # since we are just interested in the structure at this point. + seasons = fireEvent('season.info', merge = True, identifier = identifier) if seasons is not None: for season in seasons: - season['title'] = season.get('title', None) + # Make sure we are only dealing with 'tvdb' responses at this point + if season.get('primary_provider', None) != 'thetvdb': + continue season_id = season.get('id', None) if season_id is None: continue - season['identifier'] = season_id - season['parent_identifier'] = identifier - self.addToDatabase(params=season, type = "season") - # XXX: Fix so we dont have a nested list [0] (fireEvent) - try: - episodes = fireEvent('episode.info', identifier = identifier, season_identifier = season_id)[0] - except: continue + season_params = {'season_identifier': season_id} + # Calling all info providers; merge your info now for individual season + single_season = fireEvent('season.info', merge = True, identifier = identifier, params = season_params) + single_season['title'] = single_season.get('original_title', None) + single_season['identifier'] = season_id + single_season['parent_identifier'] = identifier + log.info("Adding Season %s" % season_id) + s = self.addToDatabase(params = single_season, type = "season") + + episode_params = {'season_identifier': season_id} + episodes = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params) if episodes is not None: for episode in episodes: - episode['title'] = episode.get('titles', None)[0] # XXX. [0] will create exception. FIX! + # Make sure we are only dealing with 'tvdb' responses at this point + if episode.get('primary_provider', None) != 'thetvdb': + continue episode_id = episode.get('id', None) if episode_id is None: continue - episode['identifier'] = episode_id - episode['parent_identifier'] = season['identifier'] - self.addToDatabase(params=episode, type = "episode") + try: + episode_number = int(episode.get('episodenumber', None)) + except (ValueError, TypeError): + continue + try: + absolute_number = int(episode.get('absolute_number', None)) + except (ValueError, TypeError): + absolute_number = None + + episode_params = {'season_identifier': season_id, + 'episode_identifier': episode_id, + 'episode': episode_number} + if absolute_number: + episode_params['absolute'] = absolute_number + # Calling all info providers; merge your info now for individual episode + single_episode = fireEvent('episode.info', merge = True, identifier = identifier, params = episode_params) + single_episode['title'] = single_episode.get('original_title', None) + single_episode['identifier'] = episode_id + single_episode['parent_identifier'] = single_season['identifier'] + log.info("Adding [%sx%s] %s - %s" % (season_id, + episode_number, + params['title'], + single_episode.get('original_title', ''))) + e = self.addToDatabase(params = single_episode, type = "episode") return parent diff --git a/couchpotato/core/media/show/library/episode/main.py b/couchpotato/core/media/show/library/episode/main.py index bb7ac477..e8997209 100644 --- a/couchpotato/core/media/show/library/episode/main.py +++ b/couchpotato/core/media/show/library/episode/main.py @@ -88,8 +88,9 @@ class EpisodeLibraryPlugin(LibraryBase): if library.status_id == done_status.get('id') and not force: do_update = False - info = fireEvent('episode.info', merge = True, season_identifier = parent_identifier, \ - episode_identifier = identifier) + episode_params = {'season_identifier': parent_identifier, + 'episode_identifier': identifier} + info = fireEvent('episode.info', merge = True, params = episode_params) # Don't need those here try: del info['in_wanted'] @@ -156,8 +157,7 @@ class EpisodeLibraryPlugin(LibraryBase): except: log.debug('Failed to attach to library: %s', traceback.format_exc()) - library_dict = library.to_dict(self.default_dict) - + library_dict = library.to_dict(self.default_dict) db.expire_all() return library_dict diff --git a/couchpotato/core/media/show/library/season/main.py b/couchpotato/core/media/show/library/season/main.py index b3866d3e..75f19d5a 100644 --- a/couchpotato/core/media/show/library/season/main.py +++ b/couchpotato/core/media/show/library/season/main.py @@ -63,7 +63,6 @@ class SeasonLibraryPlugin(LibraryBase): handle('library.update.season', identifier = l.identifier, default_title = toUnicode(attrs.get('title', ''))) library_dict = l.to_dict(self.default_dict) - db.expire_all() return library_dict @@ -88,8 +87,8 @@ class SeasonLibraryPlugin(LibraryBase): if library.status_id == done_status.get('id') and not force: do_update = False - info = fireEvent('season.info', merge = True, identifier = parent_identifier, \ - season_identifier = identifier) + season_params = {'season_identifier': identifier} + info = fireEvent('season.info', merge = True, identifier = parent_identifier, params = season_params) # Don't need those here try: del info['in_wanted'] @@ -148,13 +147,11 @@ class SeasonLibraryPlugin(LibraryBase): file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() library.files.append(file_obj) db.commit() - break except: log.debug('Failed to attach to library: %s', traceback.format_exc()) - library_dict = library.to_dict(self.default_dict) - + library_dict = library.to_dict(self.default_dict) db.expire_all() return library_dict diff --git a/couchpotato/core/media/show/library/show/main.py b/couchpotato/core/media/show/library/show/main.py index 141a9e0f..944ab96a 100644 --- a/couchpotato/core/media/show/library/show/main.py +++ b/couchpotato/core/media/show/library/show/main.py @@ -57,7 +57,6 @@ class ShowLibraryPlugin(LibraryBase): handle('library.update.show', identifier = l.identifier, default_title = toUnicode(attrs.get('title', ''))) library_dict = l.to_dict(self.default_dict) - db.expire_all() return library_dict diff --git a/couchpotato/core/plugins/dashboard/main.py b/couchpotato/core/plugins/dashboard/main.py index 06d32bdf..330fd959 100644 --- a/couchpotato/core/plugins/dashboard/main.py +++ b/couchpotato/core/plugins/dashboard/main.py @@ -98,11 +98,11 @@ class Dashboard(Plugin): if len(movie_ids) > 0: # Get all movie information - movies_raw = db.query(Movie) \ + movies_raw = db.query(Media) \ .options(joinedload_all('library.titles')) \ .options(joinedload_all('library.files')) \ .options(joinedload_all('files')) \ - .filter(Movie.id.in_(movie_ids)) \ + .filter(Media.id.in_(movie_ids)) \ .all() # Create dict by movie id diff --git a/couchpotato/core/providers/info/thetvdb/main.py b/couchpotato/core/providers/info/thetvdb/main.py index e0af4033..f0c7fc36 100644 --- a/couchpotato/core/providers/info/thetvdb/main.py +++ b/couchpotato/core/providers/info/thetvdb/main.py @@ -110,13 +110,15 @@ class TheTVDb(ShowProvider): return result - def getSeasonInfo(self, identifier=None, season_identifier=None): + def getSeasonInfo(self, identifier = None, params = {}): """Either return a list of all seasons or a single season by number. identifier is the show 'id' """ if not identifier: return False + season_identifier = params.get('season_identifier', None) + # season_identifier must contain the 'show id : season number' since there is no tvdb id # for season and we need a reference to both the show id and season number if season_identifier: @@ -147,10 +149,13 @@ class TheTVDb(ShowProvider): self.setCache(cache_key, result) return result - def getEpisodeInfo(self, identifier=None, season_identifier=None, episode_identifier=None): + def getEpisodeInfo(self, identifier = None, params = {}): """Either return a list of all episodes or a single episode. If episode_identifer contains an episode number to search for """ + season_identifier = params.get('season_identifier', None) + episode_identifier = params.get('episode_identifier', None) + if not identifier and season_identifier is None: return False diff --git a/couchpotato/core/providers/info/xem/__init__.py b/couchpotato/core/providers/info/xem/__init__.py new file mode 100644 index 00000000..2fc765b8 --- /dev/null +++ b/couchpotato/core/providers/info/xem/__init__.py @@ -0,0 +1,24 @@ +from .main import Xem + +def start(): + return Xem() + +config = [{ + 'name': 'xem', + 'groups': [ + { + 'tab': 'providers', + 'name': 'xem', + 'label': 'TheXem', + 'hidden': True, + 'description': 'Used for all calls to TheXem.', + 'options': [ + { + 'name': 'enabled', + 'default': True, + 'label': 'Enabled', + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/info/xem/main.py b/couchpotato/core/providers/info/xem/main.py new file mode 100644 index 00000000..0fe260d3 --- /dev/null +++ b/couchpotato/core/providers/info/xem/main.py @@ -0,0 +1,184 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.providers.info.base import ShowProvider +from couchpotato.core.helpers.encoding import tryUrlencode +import traceback + +log = CPLog(__name__) + + +class Xem(ShowProvider): + ''' + Mapping Information + =================== + + Single + ------ + You will need the id / identifier of the show e.g. tvdb-id for American Dad! is 73141 + the origin is the name of the site/entity the episode, season (and/or absolute) numbers are based on + + http://thexem.de/map/single?id=&origin=&episode=&season=&absolute= + + episode, season and absolute are all optional but it wont work if you don't provide either episode and season OR absolute in + addition you can provide destination as the name of the wished destination, if not provided it will output all available + + When a destination has two or more addresses another entry will be added as _ ... for now the second address gets the index "2" + (the first index is omitted) and so on + + http://thexem.de/map/single?id=7529&origin=anidb&season=1&episode=2&destination=trakt + { + "result":"success", + "data":{ + "trakt": {"season":1,"episode":3,"absolute":3}, + "trakt_2":{"season":1,"episode":4,"absolute":4} + }, + "message":"single mapping for 7529 on anidb." + } + + All + --- + Basically same as "single" just a little easier + The origin address is added into the output too!! + + http://thexem.de/map/all?id=7529&origin=anidb + + All Names + --------- + Get all names xem has to offer + non optional params: origin(an entity string like 'tvdb') + optional params: season, language + - season: a season number or a list like: 1,3,5 or a compare operator like ne,gt,ge,lt,le,eq and a season number. default would + return all + - language: a language string like 'us' or 'jp' default is all + - defaultNames: 1(yes) or 0(no) should the default names be added to the list ? default is 0(no) + + http://thexem.de/map/allNames?origin=tvdb&season=le1 + + { + "result": "success", + "data": { + "248812": ["Dont Trust the Bitch in Apartment 23", "Don't Trust the Bitch in Apartment 23"], + "257571": ["Nazo no Kanojo X"], + "257875": ["Lupin III - Mine Fujiko to Iu Onna", "Lupin III Fujiko to Iu Onna", "Lupin the Third - Mine Fujiko to Iu Onna"] + }, + "message": "" + } + ''' + + def __init__(self): + addEvent('show.info', self.getShowInfo, priority = 5) + addEvent('episode.info', self.getEpisodeInfo, priority = 5) + + self.config = {} + self.config['base_url'] = "http://thexem.de" + self.config['url_single'] = u"%(base_url)s/map/single?" % self.config + self.config['url_all'] = u"%(base_url)s/map/all?" % self.config + self.config['url_names'] = u"%(base_url)s/map/names?" % self.config + self.config['url_all_names'] = u"%(base_url)s/map/allNames?" % self.config + + # TODO: Also get show aliases (store as titles) + def getShowInfo(self, identifier = None): + if self.isDisabled(): + return {} + + cache_key = 'xem.cache.%s' % identifier + log.debug('Getting showInfo: %s', cache_key) + result = self.getCache(cache_key) or {} + if result: + return result + + # Create season/episode and absolute mappings + url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier) + response = self.getJsonData(url) + if response: + if response.get('result') == 'success': + data = response.get('data', None) + result = self._parse(data) + + # Create name alias mappings + url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier) + response = self.getJsonData(url) + if response: + if response.get('result') == 'success': + data = response.get('data', None) + result.update({'map_names': data}) + + self.setCache(cache_key, result) + return result + + def getEpisodeInfo(self, identifier = None, params = {}): + episode = params.get('episode', None) + if episode is None: + return False + + season_identifier = params.get('season_identifier', None) + if season_identifier is None: + return False + + episode_identifier = params.get('episode_identifier', None) + absolute = params.get('absolute', None) + + # season_identifier must contain the 'show id : season number' since there is no tvdb id + # for season and we need a reference to both the show id and season number + if season_identifier: + try: + identifier, season_identifier = season_identifier.split(':') + season = int(season_identifier) + except: return False + + result = self.getShowInfo(identifier) + map = {} + if result: + map_episode = result.get('map_episode', {}).get(season, {}).get(episode, {}) + if map_episode: + map.update({'map_episode': map_episode}) + + if absolute: + map_absolute = result.get('map_absolute', {}).get(absolute, {}) + if map_absolute: + map.update({'map_absolute': map_absolute}) + + map_names = result.get('map_names', {}).get(season, {}).get(episode, {}) + if map_names: + map.update({'map_names': map_names}) + + return map + + + def _parse(self, data, master = 'tvdb'): + '''parses xem map and returns a custom formatted dict map + + To retreive map for scene: + if 'scene' in map['map_episode'][1][1]: + print map['map_episode'][1][1]['scene']['season'] + ''' + if not isinstance(data, list): + return {} + + map = {'map_episode': {}, 'map_absolute': {}} + for maps in data: + origin = maps.pop(master, None) + if origin is None: + continue # No master origin to map to + map.get('map_episode').setdefault(origin['season'], {}).setdefault(origin['episode'], maps.copy()) + map.get('map_absolute').setdefault(origin['absolute'], maps.copy()) + + return map + + def isDisabled(self): + if __name__ == '__main__': + return False + if self.conf('enabled'): + return False + else: + return True + +#XXX: REMOVE, just for degugging +def main(): + """Simple example of using xem + """ + xem_instance = Xem() + print xem_instance.getShowInfo(identifier=73141) # (American Dad) + +if __name__ == '__main__': + main() \ No newline at end of file