From 0f97e57307ff679cd351e4da2d7d514fce3fcac9 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 10:52:53 +1200 Subject: [PATCH 01/59] Added "library.related" event and "library.query", "library.related" API calls --- couchpotato/core/media/_base/library/main.py | 38 ++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index a723de5..2eccfce 100644 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -1,10 +1,35 @@ +from couchpotato import get_db +from couchpotato.api import addApiView from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.logger import CPLog from couchpotato.core.media._base.library.base import LibraryBase +log = CPLog(__name__) + class Library(LibraryBase): def __init__(self): addEvent('library.title', self.title) + addEvent('library.related', self.related) + + addApiView('library.query', self.queryView) + addApiView('library.related', self.relatedView) + + def queryView(self, media_id, **kwargs): + db = get_db() + media = db.get('id', media_id) + + return { + 'result': fireEvent('library.query', media, single = True) + } + + def relatedView(self, media_id, **kwargs): + db = get_db() + media = db.get('id', media_id) + + return { + 'result': fireEvent('library.related', media, single = True) + } def title(self, library): return fireEvent( @@ -16,3 +41,16 @@ class Library(LibraryBase): include_identifier = False, single = True ) + + def related(self, media): + result = {media['type']: media} + + db = get_db() + cur = media + + while cur and cur.get('parent_id'): + cur = db.get('id', cur['parent_id']) + + result[cur['type']] = cur + + return result From 0d128a3525a971ec1eace0967e95423c2b308b9e Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 10:54:00 +1200 Subject: [PATCH 02/59] [TV} Fixed query/identifier event handlers and moved them to [media.show.library] --- couchpotato/core/media/show/_base/main.py | 25 --------- couchpotato/core/media/show/episode.py | 63 ---------------------- couchpotato/core/media/show/library/__init__.py | 0 couchpotato/core/media/show/library/episode.py | 71 +++++++++++++++++++++++++ couchpotato/core/media/show/library/season.py | 52 ++++++++++++++++++ couchpotato/core/media/show/library/show.py | 38 +++++++++++++ couchpotato/core/media/show/season.py | 51 ------------------ 7 files changed, 161 insertions(+), 139 deletions(-) create mode 100644 couchpotato/core/media/show/library/__init__.py create mode 100644 couchpotato/core/media/show/library/episode.py create mode 100644 couchpotato/core/media/show/library/season.py create mode 100644 couchpotato/core/media/show/library/show.py diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 7d7e637..f96ff61 100644 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -17,7 +17,6 @@ log = CPLog(__name__) class ShowBase(MediaBase): _type = 'show' - query_condenser = QueryCondenser() def __init__(self): super(ShowBase, self).__init__() @@ -35,8 +34,6 @@ class ShowBase(MediaBase): addEvent('show.add', self.add) addEvent('show.update_info', self.updateInfo) - addEvent('media.search_query', self.query) - def addView(self, **kwargs): add_dict = self.add(params = kwargs) @@ -255,25 +252,3 @@ class ShowBase(MediaBase): 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 - condensed_titles = self.query_condenser.distinct(titles) - - if condensed_titles: - # Use condensed titles if we got a valid result - titles = condensed_titles - else: - # Fallback to simplifying titles - titles = [simplifyString(title) for title in titles] - - if first: - return titles[0] if titles else None - - return titles diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index c4a0a1a..0f05f28 100644 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -13,9 +13,6 @@ autoload = 'Episode' class Episode(MediaBase): def __init__(self): - addEvent('media.search_query', self.query) - addEvent('media.identifier', self.identifier) - addEvent('show.episode.add', self.add) addEvent('show.episode.update_info', self.updateInfo) @@ -85,63 +82,3 @@ class Episode(MediaBase): self.getPoster(image_urls, existing_files) return episode - - def query(self, library, first = True, condense = True, include_identifier = True, **kwargs): - if library is list or library.get('type') != 'episode': - return - - # Get the titles of the season - if not library.get('related_libraries', {}).get('season', []): - log.warning('Invalid library, unable to determine title.') - return - - titles = fireEvent( - 'media.search_query', - library['related_libraries']['season'][0], - first=False, - include_identifier=include_identifier, - condense=condense, - - single=True - ) - - identifier = fireEvent('media.identifier', library, single = True) - - # Add episode identifier to titles - if include_identifier and identifier.get('episode'): - titles = [title + ('E%02d' % identifier['episode']) for title in titles] - - - if first: - return titles[0] if titles else None - - return titles - - - def identifier(self, media): - if media.get('type') != 'episode': - return - - identifier = { - 'season': None, - 'episode': None - } - - scene_map = media['info'].get('map_episode', {}).get('scene') - - if scene_map: - # Use scene mappings if they are available - identifier['season'] = scene_map.get('season_nr') - identifier['episode'] = scene_map.get('episode_nr') - else: - # Fallback to normal season/episode numbers - identifier['season'] = media['info'].get('season_number') - identifier['episode'] = media['info'].get('number') - - - # Cast identifiers to integers - # TODO this will need changing to support identifiers with trailing 'a', 'b' characters - identifier['season'] = tryInt(identifier['season'], None) - identifier['episode'] = tryInt(identifier['episode'], None) - - return identifier diff --git a/couchpotato/core/media/show/library/__init__.py b/couchpotato/core/media/show/library/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/show/library/episode.py b/couchpotato/core/media/show/library/episode.py new file mode 100644 index 0000000..7161f2d --- /dev/null +++ b/couchpotato/core/media/show/library/episode.py @@ -0,0 +1,71 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.library.base import LibraryBase + +log = CPLog(__name__) + +autoload = 'EpisodeLibraryPlugin' + + +class EpisodeLibraryPlugin(LibraryBase): + def __init__(self): + addEvent('library.query', self.query) + addEvent('library.identifier', self.identifier) + + def query(self, media, first = True, condense = True, include_identifier = True, **kwargs): + if media.get('type') != 'episode': + return + + related = fireEvent('library.related', media, single = True) + + # Get season titles + titles = fireEvent( + 'library.query', related['season'], + + first = False, + include_identifier = include_identifier, + condense = condense, + + single = True + ) + + # Add episode identifier to titles + if include_identifier: + identifier = fireEvent('library.identifier', media, single = True) + + if identifier and identifier.get('episode'): + titles = [title + ('E%02d' % identifier['episode']) for title in titles] + + if first: + return titles[0] if titles else None + + return titles + + def identifier(self, media): + if media.get('type') != 'episode': + return + + identifier = { + 'season': None, + 'episode': None + } + + # TODO identifier mapping + # scene_map = media['info'].get('map_episode', {}).get('scene') + + # if scene_map: + # # Use scene mappings if they are available + # identifier['season'] = scene_map.get('season_nr') + # identifier['episode'] = scene_map.get('episode_nr') + # else: + # Fallback to normal season/episode numbers + identifier['season'] = media['info'].get('season_number') + identifier['episode'] = media['info'].get('number') + + # Cast identifiers to integers + # TODO this will need changing to support identifiers with trailing 'a', 'b' characters + identifier['season'] = tryInt(identifier['season'], None) + identifier['episode'] = tryInt(identifier['episode'], None) + + return identifier diff --git a/couchpotato/core/media/show/library/season.py b/couchpotato/core/media/show/library/season.py new file mode 100644 index 0000000..228d143 --- /dev/null +++ b/couchpotato/core/media/show/library/season.py @@ -0,0 +1,52 @@ +from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.library.base import LibraryBase + +log = CPLog(__name__) + +autoload = 'SeasonLibraryPlugin' + + +class SeasonLibraryPlugin(LibraryBase): + def __init__(self): + addEvent('library.query', self.query) + addEvent('library.identifier', self.identifier) + + def query(self, media, first = True, condense = True, include_identifier = True, **kwargs): + if media.get('type') != 'season': + return + + related = fireEvent('library.related', media, single = True) + + # Get show titles + titles = fireEvent( + 'library.query', related['show'], + + first = False, + condense = condense, + + single = True + ) + + # TODO map_names + + # Add season identifier to titles + if include_identifier: + identifier = fireEvent('library.identifier', media, single = True) + + if identifier and identifier.get('season') is not None: + titles = [title + (' S%02d' % identifier['season']) for title in titles] + + if first: + return titles[0] if titles else None + + return titles + + def identifier(self, media): + if media.get('type') != 'season': + return + + return { + 'season': tryInt(media['info']['number'], None) + } diff --git a/couchpotato/core/media/show/library/show.py b/couchpotato/core/media/show/library/show.py new file mode 100644 index 0000000..168804c --- /dev/null +++ b/couchpotato/core/media/show/library/show.py @@ -0,0 +1,38 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.helpers.encoding import simplifyString +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.library.base import LibraryBase +from qcond import QueryCondenser + +log = CPLog(__name__) + +autoload = 'ShowLibraryPlugin' + + +class ShowLibraryPlugin(LibraryBase): + query_condenser = QueryCondenser() + + def __init__(self): + addEvent('library.query', self.query) + + def query(self, media, first = True, condense = True, include_identifier = True, **kwargs): + if media.get('type') != 'show': + return + + titles = media['info']['titles'] + + if condense: + # Use QueryCondenser to build a list of optimal search titles + condensed_titles = self.query_condenser.distinct(titles) + + if condensed_titles: + # Use condensed titles if we got a valid result + titles = condensed_titles + else: + # Fallback to simplifying titles + titles = [simplifyString(title) for title in titles] + + if first: + return titles[0] if titles else None + + return titles diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index 9c6f8f0..a2dde78 100644 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -13,9 +13,6 @@ autoload = 'Season' class Season(MediaBase): def __init__(self): - addEvent('media.search_query', self.query) - addEvent('media.identifier', self.identifier) - addEvent('show.season.add', self.add) addEvent('show.season.update_info', self.updateInfo) @@ -87,51 +84,3 @@ class Season(MediaBase): self.getPoster(image_urls, existing_files) return season - - def query(self, library, first = True, condense = True, include_identifier = True, **kwargs): - if library is list or library.get('type') != 'season': - return - - # Get the titles of the show - if not library.get('related_libraries', {}).get('show', []): - log.warning('Invalid library, unable to determine title.') - return - - titles = fireEvent( - 'media._search_query', - library['related_libraries']['show'][0], - first=False, - condense=condense, - - single=True - ) - - # Add season map_names if they exist - if 'map_names' in library['info']: - season_names = library['info']['map_names'].get(str(library['season_number']), {}) - - # Add titles from all locations - # TODO only add name maps from a specific location - for location, names in season_names.items(): - titles += [name for name in names if name and name not in titles] - - - identifier = fireEvent('media.identifier', library, single = True) - - # Add season identifier to titles - if include_identifier and identifier.get('season') is not None: - titles = [title + (' S%02d' % identifier['season']) for title in titles] - - - if first: - return titles[0] if titles else None - - return titles - - def identifier(self, library): - if library.get('type') != 'season': - return - - return { - 'season': tryInt(library['season_number'], None) - } From 4efdca91d57d65d9320bbe4866cf9ff0e81d18c2 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 11:34:32 +1200 Subject: [PATCH 03/59] [TV] Added temporary TV qualities --- couchpotato/core/plugins/quality/main.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 6304924..3c80966 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -32,7 +32,25 @@ class QualityPlugin(Plugin): {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr'], 'ext':[]}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':[]}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':[]}, - {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]} + {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':[]}, + + # TODO come back to this later, think this could be handled better, this is starting to get out of hand.... + # BluRay + {'identifier': 'bluray_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'bluray_720p', 'hd': True, 'size': (800, 5000), 'label': 'BluRay - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + # BDRip + {'identifier': 'bdrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'bdrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BDRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + # BRRip + {'identifier': 'brrip_1080p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'brrip_720p', 'hd': True, 'size': (800, 5000), 'label': 'BRRip - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + # WEB-DL + {'identifier': 'webdl_1080p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'webdl_720p', 'hd': True, 'size': (800, 5000), 'label': 'WEB-DL - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'webdl_480p', 'hd': True, 'size': (100, 5000), 'label': 'WEB-DL - 480p', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + # HDTV + {'identifier': 'hdtv_720p', 'hd': True, 'size': (800, 5000), 'label': 'HDTV - 720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv']}, + {'identifier': 'hdtv_sd', 'hd': False, 'size': (100, 1000), 'label': 'HDTV - SD', 'width': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'mp4', 'avi']}, ] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] threed_tags = { From 050d8ccfdabb712e6a7ae51afaae2f390f3f4ab7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 11:58:27 +1200 Subject: [PATCH 04/59] Added "library.root" event, fixes to "matcher", "release" and "score" to use "library.root" + handle missing "year" --- couchpotato/core/media/_base/library/main.py | 10 ++++++++++ couchpotato/core/media/_base/matcher/main.py | 6 +++--- couchpotato/core/plugins/release/main.py | 2 +- couchpotato/core/plugins/score/main.py | 2 +- couchpotato/core/plugins/score/scores.py | 2 +- 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index 2eccfce..b90f4c3 100644 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -11,6 +11,7 @@ class Library(LibraryBase): def __init__(self): addEvent('library.title', self.title) addEvent('library.related', self.related) + addEvent('library.root', self.root) addApiView('library.query', self.queryView) addApiView('library.related', self.relatedView) @@ -54,3 +55,12 @@ class Library(LibraryBase): result[cur['type']] = cur return result + + def root(self, media): + db = get_db() + cur = media + + while cur and cur.get('parent_id'): + cur = db.get('id', cur['parent_id']) + + return cur diff --git a/couchpotato/core/media/_base/matcher/main.py b/couchpotato/core/media/_base/matcher/main.py index 2034249..64e13ae 100644 --- a/couchpotato/core/media/_base/matcher/main.py +++ b/couchpotato/core/media/_base/matcher/main.py @@ -40,7 +40,7 @@ class Matcher(MatcherBase): return False def correctTitle(self, chain, media): - root_library = media['library']['root_library'] + root = fireEvent('library.root', media, single = True) if 'show_name' not in chain.info or not len(chain.info['show_name']): log.info('Wrong: missing show name in parsed result') @@ -50,10 +50,10 @@ class Matcher(MatcherBase): chain_words = [x.lower() for x in chain.info['show_name']] # Build a list of possible titles of the media we are searching for - titles = root_library['info']['titles'] + titles = root['info']['titles'] # Add year suffix titles (will result in ['', ' ', '', ...]) - suffixes = [None, root_library['info']['year']] + suffixes = [None, root['info']['year']] titles = [ title + ((' %s' % suffix) if suffix else '') diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 08f0248..7f389f2 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -315,7 +315,7 @@ class Release(Plugin): rls['download_info'] = download_result db.update(rls) - log_movie = '%s (%s) in %s' % (getTitle(media), media['info']['year'], rls['quality']) + log_movie = '%s (%s) in %s' % (getTitle(media), media['info'].get('year'), rls['quality']) snatch_message = 'Snatched "%s": %s' % (data.get('name'), log_movie) log.info(snatch_message) fireEvent('%s.snatched' % data['type'], message = snatch_message, data = rls) diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py index e6fef25..08a1855 100644 --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -24,7 +24,7 @@ class Score(Plugin): try: preferred_words = removeDuplicate(preferred_words + splitString(movie['category']['preferred'].lower())) except: pass - score = nameScore(toUnicode(nzb['name']), movie['info']['year'], preferred_words) + score = nameScore(toUnicode(nzb['name']), movie['info'].get('year'), preferred_words) for movie_title in movie['info']['titles']: score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title)) diff --git a/couchpotato/core/plugins/score/scores.py b/couchpotato/core/plugins/score/scores.py index a53608c..ddae430 100644 --- a/couchpotato/core/plugins/score/scores.py +++ b/couchpotato/core/plugins/score/scores.py @@ -44,7 +44,7 @@ def nameScore(name, year, preferred_words): score += add # points if the year is correct - if str(year) in name: + if year and str(year) in name: score += 5 # Contains preferred word From 0925dd08bc6ad40fc88a047e9d9359e1cd9fb319 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 11:59:27 +1200 Subject: [PATCH 05/59] [TV] Split searcher into separate modules, searching/snatching mostly working again --- couchpotato/core/media/show/__init__.py | 7 +- couchpotato/core/media/show/matcher.py | 2 +- couchpotato/core/media/show/searcher.py | 232 ----------------------- couchpotato/core/media/show/searcher/__init__.py | 0 couchpotato/core/media/show/searcher/episode.py | 137 +++++++++++++ couchpotato/core/media/show/searcher/season.py | 38 ++++ couchpotato/core/media/show/searcher/show.py | 83 ++++++++ 7 files changed, 265 insertions(+), 234 deletions(-) delete mode 100644 couchpotato/core/media/show/searcher.py create mode 100644 couchpotato/core/media/show/searcher/__init__.py create mode 100644 couchpotato/core/media/show/searcher/episode.py create mode 100644 couchpotato/core/media/show/searcher/season.py create mode 100644 couchpotato/core/media/show/searcher/show.py diff --git a/couchpotato/core/media/show/__init__.py b/couchpotato/core/media/show/__init__.py index 89af436..89bfef6 100644 --- a/couchpotato/core/media/show/__init__.py +++ b/couchpotato/core/media/show/__init__.py @@ -2,5 +2,10 @@ from couchpotato.core.media import MediaBase class ShowTypeBase(MediaBase): - _type = 'show' + + def getType(self): + if hasattr(self, 'type') and self.type != self._type: + return '%s.%s' % (self._type, self.type) + + return self._type diff --git a/couchpotato/core/media/show/matcher.py b/couchpotato/core/media/show/matcher.py index 4056d64..7df36d1 100644 --- a/couchpotato/core/media/show/matcher.py +++ b/couchpotato/core/media/show/matcher.py @@ -96,7 +96,7 @@ class Episode(Base): log.info2('Wrong: releases with identifier ranges are not supported yet') return False - required = fireEvent('media.identifier', media['library'], single = True) + required = fireEvent('library.identifier', media, single = True) # TODO - Support air by date episodes # TODO - Support episode parts diff --git a/couchpotato/core/media/show/searcher.py b/couchpotato/core/media/show/searcher.py deleted file mode 100644 index 255d12d..0000000 --- a/couchpotato/core/media/show/searcher.py +++ /dev/null @@ -1,232 +0,0 @@ -import time -from couchpotato import Env, get_db -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.variable import getTitle, toIterable -from couchpotato.core.logger import CPLog -from couchpotato.core.media._base.searcher.base import SearcherBase -from couchpotato.core.media._base.searcher.main import SearchSetupError -from couchpotato.core.media.show import ShowTypeBase -from qcond import QueryCondenser - -log = CPLog(__name__) - -autoload = 'ShowSearcher' - - -class ShowSearcher(SearcherBase, ShowTypeBase): - - type = ['show', 'season', 'episode'] - - in_progress = False - - def __init__(self): - super(ShowSearcher, self).__init__() - - self.query_condenser = QueryCondenser() - - addEvent('season.searcher.single', self.singleSeason) - addEvent('episode.searcher.single', self.singleEpisode) - - addEvent('searcher.correct_release', self.correctRelease) - addEvent('searcher.get_search_title', self.getSearchTitle) - - - - def single(self, media, search_protocols = None, manual = False): - - # Find out search type - try: - if not search_protocols: - search_protocols = fireEvent('searcher.protocols', single = True) - except SearchSetupError: - return - - if not media['profile_id'] or media['status'] == 'done': - log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.') - return - - show_title = fireEvent('media.search_query', media, condense = False, single = True) - - fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title) - - media = self.extendShow(media) - - db = get_db() - - profile = db.get('id', media['profile_id']) - quality_order = fireEvent('quality.order', single = True) - - seasons = media.get('seasons', {}) - for sx in seasons: - - # Skip specials for now TODO: set status for specials to skipped by default - if sx == 0: continue - - season = seasons.get(sx) - - # Check if full season can be downloaded TODO: add - season_success = self.singleSeason(season, media, profile) - - # Do each episode seperately - if not season_success: - episodes = season.get('episodes', {}) - for ex in episodes: - episode = episodes.get(ex) - - self.singleEpisode(episode, season, media, profile, quality_order, search_protocols) - - # TODO - return - - # TODO - return - - fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) - - def singleSeason(self, media, show, profile): - - # Check if any episode is already snatched - active = 0 - episodes = media.get('episodes', {}) - for ex in episodes: - episode = episodes.get(ex) - - if episode.get('status') in ['active']: - active += 1 - - if active != len(episodes): - return False - - # Try and search for full season - # TODO: - - return False - - def singleEpisode(self, media, season, show, profile, quality_order, search_protocols = None, manual = False): - - - # TODO: check episode status - - - # TODO: check air date - #if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): - # too_early_to_search.append(quality_type['quality']['identifier']) - # return - - ret = False - has_better_quality = None - found_releases = [] - too_early_to_search = [] - - releases = fireEvent('release.for_media', media['_id'], single = True) - show_title = getTitle(show) - episode_identifier = '%s S%02d%s' % (show_title, season['info'].get('number', 0), "E%02d" % media['info'].get('number')) - - # Add parents - media['show'] = show - media['season'] = season - - index = 0 - for q_identifier in profile.get('qualities'): - quality_custom = { - 'quality': q_identifier, - 'finish': profile['finish'][index], - 'wait_for': profile['wait_for'][index], - '3d': profile['3d'][index] if profile.get('3d') else False - } - - has_better_quality = 0 - - # See if better quality is available - for release in releases: - if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: - has_better_quality += 1 - - # Don't search for quality lower then already available. - if has_better_quality is 0: - - log.info('Searching for %s in %s', (episode_identifier, q_identifier)) - quality = fireEvent('quality.single', identifier = q_identifier, single = True) - quality['custom'] = quality_custom - - results = fireEvent('searcher.search', search_protocols, media, quality, single = True) - if len(results) == 0: - log.debug('Nothing found for %s in %s', (episode_identifier, q_identifier)) - - # Add them to this movie releases list - found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) - - # Try find a valid result and download it - if fireEvent('release.try_download_result', results, media, quality, manual, single = True): - ret = True - - # Remove releases that aren't found anymore - for release in releases: - if release.get('status') == 'available' and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('id'), single = True) - else: - log.info('Better quality (%s) already available or snatched for %s', (q_identifier, episode_identifier)) - fireEvent('media.restatus', media['_id']) - break - - # Break if CP wants to shut down - if self.shuttingDown() or ret: - break - - if len(too_early_to_search) > 0: - log.info2('Too early to search for %s, %s', (too_early_to_search, episode_identifier)) - - def correctRelease(self, release = None, media = None, quality = None, **kwargs): - - if media.get('type') not in ['season', 'episode']: return - - retention = Env.setting('retention', section = 'nzb') - - if release.get('seeders') is None and 0 < retention < release.get('age', 0): - log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name'])) - return False - - # Check for required and ignored words - if not fireEvent('searcher.correct_words', release['name'], media, single = True): - return False - - # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) - match = fireEvent('matcher.match', release, media, quality, single = True) - if match: - return match.weight - - return False - - def extendShow(self, media): - - db = get_db() - - seasons = db.get_many('media_children', media['_id'], with_doc = True) - - media['seasons'] = {} - - for sx in seasons: - season = sx['doc'] - - # Add episode info - season['episodes'] = {} - episodes = db.get_many('media_children', sx['_id'], with_doc = True) - - for se in episodes: - episode = se['doc'] - season['episodes'][episode['info'].get('number')] = episode - - # Add season to show - media['seasons'][season['info'].get('number', 0)] = season - - return media - - def searchAll(self): - pass - - def getSearchTitle(self, media): - # TODO: this should be done for season and episode - if media['type'] == 'season': - return getTitle(media) - elif media['type'] == 'episode': - return getTitle(media) diff --git a/couchpotato/core/media/show/searcher/__init__.py b/couchpotato/core/media/show/searcher/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py new file mode 100644 index 0000000..1e6b253 --- /dev/null +++ b/couchpotato/core/media/show/searcher/episode.py @@ -0,0 +1,137 @@ +from couchpotato import fireEvent, get_db, Env +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import SearchSetupError +from couchpotato.core.media.show import ShowTypeBase + +log = CPLog(__name__) + +autoload = 'EpisodeSearcher' + + +class EpisodeSearcher(SearcherBase, ShowTypeBase): + type = 'episode' + + in_progress = False + + def __init__(self): + super(EpisodeSearcher, self).__init__() + + addEvent('%s.searcher.single' % self.getType(), self.single) + addEvent('searcher.correct_release', self.correctRelease) + + addApiView('%s.searcher.single' % self.getType(), self.singleView) + + def singleView(self, media_id, **kwargs): + db = get_db() + media = db.get('id', media_id) + + return { + 'result': fireEvent('%s.searcher.single' % self.getType(), media, single = True) + } + + def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False): + db = get_db() + + related = fireEvent('library.related', media, single = True) + + # TODO search_protocols, profile, quality_order can be moved to a base method + # Find out search type + try: + if not search_protocols: + search_protocols = fireEvent('searcher.protocols', single = True) + except SearchSetupError: + return + + if not profile and related['show']['profile_id']: + profile = db.get('id', related['show']['profile_id']) + + if not quality_order: + quality_order = fireEvent('quality.order', single = True) + + # TODO: check episode status + # TODO: check air date + #if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): + # too_early_to_search.append(quality_type['quality']['identifier']) + # return + + ret = False + has_better_quality = None + found_releases = [] + too_early_to_search = [] + + releases = fireEvent('release.for_media', media['_id'], single = True) + query = fireEvent('library.query', media, condense = False, single = True) + + index = 0 + for q_identifier in profile.get('qualities'): + quality_custom = { + 'quality': q_identifier, + 'finish': profile['finish'][index], + 'wait_for': profile['wait_for'][index], + '3d': profile['3d'][index] if profile.get('3d') else False + } + + has_better_quality = 0 + + # See if better quality is available + for release in releases: + if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: + has_better_quality += 1 + + # Don't search for quality lower then already available. + if has_better_quality is 0: + + log.info('Searching for %s in %s', (query, q_identifier)) + quality = fireEvent('quality.single', identifier = q_identifier, single = True) + quality['custom'] = quality_custom + + results = fireEvent('searcher.search', search_protocols, media, quality, single = True) + if len(results) == 0: + log.debug('Nothing found for %s in %s', (query, q_identifier)) + + # Add them to this movie releases list + found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) + + # Try find a valid result and download it + if fireEvent('release.try_download_result', results, media, quality, single = True): + ret = True + + # Remove releases that aren't found anymore + for release in releases: + if release.get('status') == 'available' and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('id'), single = True) + else: + log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query)) + fireEvent('media.restatus', media['_id']) + break + + # Break if CP wants to shut down + if self.shuttingDown() or ret: + break + + if len(too_early_to_search) > 0: + log.info2('Too early to search for %s, %s', (too_early_to_search, query)) + + def correctRelease(self, release = None, media = None, quality = None, **kwargs): + if media.get('type') != 'episode': + return + + retention = Env.setting('retention', section = 'nzb') + + if release.get('seeders') is None and 0 < retention < release.get('age', 0): + log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name'])) + return False + + # Check for required and ignored words + if not fireEvent('searcher.correct_words', release['name'], media, single = True): + return False + + # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) + match = fireEvent('matcher.match', release, media, quality, single = True) + if match: + return match.weight + + return False diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py new file mode 100644 index 0000000..64fd263 --- /dev/null +++ b/couchpotato/core/media/show/searcher/season.py @@ -0,0 +1,38 @@ +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media.show import ShowTypeBase + +log = CPLog(__name__) + +autoload = 'SeasonSearcher' + + +class SeasonSearcher(SearcherBase, ShowTypeBase): + type = 'season' + + in_progress = False + + def __init__(self): + super(SeasonSearcher, self).__init__() + + addEvent('%s.searcher.single' % self.getType(), self.single) + + def single(self, media, show, profile): + + # Check if any episode is already snatched + active = 0 + episodes = media.get('episodes', {}) + for ex in episodes: + episode = episodes.get(ex) + + if episode.get('status') in ['active']: + active += 1 + + if active != len(episodes): + return False + + # Try and search for full season + # TODO: + + return False diff --git a/couchpotato/core/media/show/searcher/show.py b/couchpotato/core/media/show/searcher/show.py new file mode 100644 index 0000000..49fb775 --- /dev/null +++ b/couchpotato/core/media/show/searcher/show.py @@ -0,0 +1,83 @@ +from couchpotato import get_db +from couchpotato.core.event import fireEvent, addEvent +from couchpotato.core.helpers.variable import getTitle +from couchpotato.core.logger import CPLog +from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media._base.searcher.main import SearchSetupError +from couchpotato.core.media.show import ShowTypeBase + +log = CPLog(__name__) + +autoload = 'ShowSearcher' + + +class ShowSearcher(SearcherBase, ShowTypeBase): + type = 'show' + + in_progress = False + + def __init__(self): + super(ShowSearcher, self).__init__() + + addEvent('%s.searcher.single' % self.getType(), self.single) + + addEvent('searcher.get_search_title', self.getSearchTitle) + + def single(self, media, search_protocols = None, manual = False): + # Find out search type + try: + if not search_protocols: + search_protocols = fireEvent('searcher.protocols', single = True) + except SearchSetupError: + return + + if not media['profile_id'] or media['status'] == 'done': + log.debug('Show doesn\'t have a profile or already done, assuming in manage tab.') + return + + show_title = fireEvent('media.search_query', media, condense = False, single = True) + + fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title) + + media = self.extendShow(media) + + db = get_db() + + profile = db.get('id', media['profile_id']) + quality_order = fireEvent('quality.order', single = True) + + seasons = media.get('seasons', {}) + for sx in seasons: + + # Skip specials for now TODO: set status for specials to skipped by default + if sx == 0: continue + + season = seasons.get(sx) + + # Check if full season can be downloaded TODO: add + season_success = fireEvent('show.season.searcher.single', season, media, profile) + + # Do each episode seperately + if not season_success: + episodes = season.get('episodes', {}) + for ex in episodes: + episode = episodes.get(ex) + + fireEvent('show.episode.searcher.single', episode, season, media, profile, quality_order, search_protocols) + + # TODO + return + + # TODO + return + + fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) + + def getSearchTitle(self, media): + if media.get('type') != 'show': + related = fireEvent('library.related', media, single = True) + show = related['show'] + else: + show = media + + return getTitle(show) From e885ade131694a8f2faa5ef56c77dfaf83bf0b02 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 13:43:31 +1200 Subject: [PATCH 06/59] [TV] Fixed show posters --- couchpotato/core/media/show/_base/main.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/_base/main.py diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py old mode 100644 new mode 100755 index f96ff61..da70c3d --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -242,11 +242,10 @@ class ShowBase(MediaBase): # Update image file image_urls = info.get('images', []) - existing_files = media.get('files', {}) - self.getPoster(image_urls, existing_files) - db.update(media) + self.getPoster(media, image_urls) + db.update(media) return media except: log.error('Failed update media: %s', traceback.format_exc()) From 7ae178e2a607a0a947f9a3e0d345f1cbf3c34d73 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 13:44:18 +1200 Subject: [PATCH 07/59] Fixed MediaBase.getPoster(), switched MovieBase to use this generic method --- couchpotato/core/media/__init__.py | 9 ++++++--- couchpotato/core/media/movie/_base/main.py | 32 +++--------------------------- 2 files changed, 9 insertions(+), 32 deletions(-) mode change 100644 => 100755 couchpotato/core/media/__init__.py mode change 100644 => 100755 couchpotato/core/media/movie/_base/main.py diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py old mode 100644 new mode 100755 index 4a3eb68..4e319fc --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -65,10 +65,13 @@ class MediaBase(Plugin): return def_title or 'UNKNOWN' - def getPoster(self, image_urls, existing_files): - image_type = 'poster' + def getPoster(self, media, image_urls): + if 'files' not in media: + media['files'] = {} + + existing_files = media['files'] - # Remove non-existing files + image_type = 'poster' file_type = 'image_%s' % image_type # Make existing unique diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py old mode 100644 new mode 100755 index 4c5c2f2..6ad85af --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -301,37 +301,11 @@ class MovieBase(MovieTypeBase): media['title'] = def_title # Files - images = info.get('images', []) - media['files'] = media.get('files', {}) - for image_type in ['poster']: - - # Remove non-existing files - file_type = 'image_%s' % image_type - existing_files = list(set(media['files'].get(file_type, []))) - for ef in media['files'].get(file_type, []): - if not os.path.isfile(ef): - existing_files.remove(ef) - - # Replace new files list - media['files'][file_type] = existing_files - if len(existing_files) == 0: - del media['files'][file_type] - - # Loop over type - for image in images.get(image_type, []): - if not isinstance(image, (str, unicode)): - continue - - if file_type not in media['files'] or len(media['files'].get(file_type, [])) == 0: - file_path = fireEvent('file.download', url = image, single = True) - if file_path: - media['files'][file_type] = [file_path] - break - else: - break + image_urls = info.get('images', []) - db.update(media) + self.getPoster(media, image_urls) + db.update(media) return media except: log.error('Failed update media: %s', traceback.format_exc()) From 2d5a3e75643ee39280be54fe5eb84d66c3c454a1 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 16:56:09 +1200 Subject: [PATCH 08/59] Added "library.tree" event/api call --- couchpotato/core/media/_base/library/main.py | 41 ++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) mode change 100644 => 100755 couchpotato/core/media/_base/library/main.py diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py old mode 100644 new mode 100755 index b90f4c3..d20342f --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -11,10 +11,13 @@ class Library(LibraryBase): def __init__(self): addEvent('library.title', self.title) addEvent('library.related', self.related) + addEvent('library.tree', self.tree) + addEvent('library.root', self.root) addApiView('library.query', self.queryView) addApiView('library.related', self.relatedView) + addApiView('library.tree', self.treeView) def queryView(self, media_id, **kwargs): db = get_db() @@ -32,6 +35,14 @@ class Library(LibraryBase): 'result': fireEvent('library.related', media, single = True) } + def treeView(self, media_id, **kwargs): + db = get_db() + media = db.get('id', media_id) + + return { + 'result': fireEvent('library.tree', media, single = True) + } + def title(self, library): return fireEvent( 'library.query', @@ -64,3 +75,33 @@ class Library(LibraryBase): cur = db.get('id', cur['parent_id']) return cur + + def tree(self, media): + result = media + + db = get_db() + + # TODO this probably should be using an index? + items = [ + item['doc'] + for item in db.all('media', with_doc = True) + if item['doc'].get('parent_id') == media['_id'] + ] + + keys = [] + + for item in items: + key = item['type'] + 's' + + if key not in result: + result[key] = {} + + if key not in keys: + keys.append(key) + + result[key][item['_id']] = fireEvent('library.tree', item, single = True) + + for key in keys: + result[key] = result[key].values() + + return result From d787cb0cdbda1b32a159b875499c610687a04124 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 16:58:04 +1200 Subject: [PATCH 09/59] [TV] Build out basic show interface with episode list --- .../core/media/show/_base/static/1_wanted.js | 2 +- couchpotato/core/media/show/_base/static/list.js | 635 ++++++++++++ couchpotato/core/media/show/_base/static/show.css | 1077 ++++++++++++++++++++ .../core/media/show/_base/static/show.episodes.js | 109 ++ couchpotato/core/media/show/_base/static/show.js | 347 +++++++ 5 files changed, 2169 insertions(+), 1 deletion(-) mode change 100644 => 100755 couchpotato/core/media/show/_base/static/1_wanted.js create mode 100755 couchpotato/core/media/show/_base/static/list.js create mode 100755 couchpotato/core/media/show/_base/static/show.css create mode 100755 couchpotato/core/media/show/_base/static/show.episodes.js create mode 100755 couchpotato/core/media/show/_base/static/show.js diff --git a/couchpotato/core/media/show/_base/static/1_wanted.js b/couchpotato/core/media/show/_base/static/1_wanted.js old mode 100644 new mode 100755 index e7b308b..2400071 --- a/couchpotato/core/media/show/_base/static/1_wanted.js +++ b/couchpotato/core/media/show/_base/static/1_wanted.js @@ -12,7 +12,7 @@ Page.Shows = new Class({ if(!self.wanted){ // Wanted movies - self.wanted = new MovieList({ + self.wanted = new ShowList({ 'identifier': 'wanted', 'status': 'active', 'type': 'show', diff --git a/couchpotato/core/media/show/_base/static/list.js b/couchpotato/core/media/show/_base/static/list.js new file mode 100755 index 0000000..a32e9b7 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/list.js @@ -0,0 +1,635 @@ +var ShowList = new Class({ + + Implements: [Events, Options], + + options: { + navigation: true, + limit: 50, + load_more: true, + loader: true, + menu: [], + add_new: false, + force_view: false + }, + + movies: [], + movies_added: {}, + total_movies: 0, + letters: {}, + filter: null, + + initialize: function(options){ + var self = this; + self.setOptions(options); + + self.offset = 0; + self.filter = self.options.filter || { + 'starts_with': null, + 'search': null + }; + + self.el = new Element('div.shows').adopt( + self.title = self.options.title ? new Element('h2', { + 'text': self.options.title, + 'styles': {'display': 'none'} + }) : null, + self.description = self.options.description ? new Element('div.description', { + 'html': self.options.description, + 'styles': {'display': 'none'} + }) : null, + self.movie_list = new Element('div'), + self.load_more = self.options.load_more ? new Element('a.load_more', { + 'events': { + 'click': self.loadMore.bind(self) + } + }) : null + ); + + if($(window).getSize().x <= 480 && !self.options.force_view) + self.changeView('list'); + else + self.changeView(self.getSavedView() || self.options.view || 'details'); + + self.getMovies(); + + App.on('movie.added', self.movieAdded.bind(self)); + App.on('movie.deleted', self.movieDeleted.bind(self)) + }, + + movieDeleted: function(notification){ + var self = this; + + if(self.movies_added[notification.data._id]){ + self.movies.each(function(movie){ + if(movie.get('_id') == notification.data._id){ + movie.destroy(); + delete self.movies_added[notification.data._id]; + self.setCounter(self.counter_count-1); + self.total_movies--; + } + }) + } + + self.checkIfEmpty(); + }, + + movieAdded: function(notification){ + var self = this; + + self.fireEvent('movieAdded', notification); + if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){ + window.scroll(0,0); + self.createMovie(notification.data, 'top'); + self.setCounter(self.counter_count+1); + + self.checkIfEmpty(); + } + }, + + create: function(){ + var self = this; + + // Create the alphabet nav + if(self.options.navigation) + self.createNavigation(); + + if(self.options.load_more) + self.scrollspy = new ScrollSpy({ + min: function(){ + var c = self.load_more.getCoordinates(); + return c.top - window.document.getSize().y - 300 + }, + onEnter: self.loadMore.bind(self) + }); + + self.created = true; + }, + + addMovies: function(movies, total){ + var self = this; + + if(!self.created) self.create(); + + // do scrollspy + if(movies.length < self.options.limit && self.scrollspy){ + self.load_more.hide(); + self.scrollspy.stop(); + } + + Object.each(movies, function(movie){ + self.createMovie(movie); + }); + + self.total_movies += total; + self.setCounter(total); + + }, + + setCounter: function(count){ + var self = this; + + if(!self.navigation_counter) return; + + self.counter_count = count; + self.navigation_counter.set('text', (count || 0) + ' movies'); + + if (self.empty_message) { + self.empty_message.destroy(); + self.empty_message = null; + } + + if(self.total_movies && count == 0 && !self.empty_message){ + var message = (self.filter.search ? 'for "'+self.filter.search+'"' : '') + + (self.filter.starts_with ? ' in '+self.filter.starts_with+'' : ''); + + self.empty_message = new Element('.message', { + 'html': 'No movies found ' + message + '.
' + }).grab( + new Element('a', { + 'text': 'Reset filter', + 'events': { + 'click': function(){ + self.filter = { + 'starts_with': null, + 'search': null + }; + self.navigation_search_input.set('value', ''); + self.reset(); + self.activateLetter(); + self.getMovies(true); + self.last_search_value = ''; + } + } + }) + ).inject(self.movie_list); + + } + + }, + + createMovie: function(movie, inject_at){ + var self = this; + var m = new Show(self, { + 'actions': self.options.actions, + 'view': self.current_view, + 'onSelect': self.calculateSelected.bind(self) + }, movie); + + $(m).inject(self.movie_list, inject_at || 'bottom'); + + m.fireEvent('injected'); + + self.movies.include(m); + self.movies_added[movie._id] = true; + }, + + createNavigation: function(){ + var self = this; + var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + + self.el.addClass('with_navigation'); + + self.navigation = new Element('div.alph_nav').adopt( + self.mass_edit_form = new Element('div.mass_edit_form').adopt( + new Element('span.select').adopt( + self.mass_edit_select = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': self.massEditToggleAll.bind(self) + } + }), + self.mass_edit_selected = new Element('span.count', {'text': 0}), + self.mass_edit_selected_label = new Element('span', {'text': 'selected'}) + ), + new Element('div.quality').adopt( + self.mass_edit_quality = new Element('select'), + new Element('a.button.orange', { + 'text': 'Change quality', + 'events': { + 'click': self.changeQualitySelected.bind(self) + } + }) + ), + new Element('div.delete').adopt( + new Element('span[text=or]'), + new Element('a.button.red', { + 'text': 'Delete', + 'events': { + 'click': self.deleteSelected.bind(self) + } + }) + ), + new Element('div.refresh').adopt( + new Element('span[text=or]'), + new Element('a.button.green', { + 'text': 'Refresh', + 'events': { + 'click': self.refreshSelected.bind(self) + } + }) + ) + ), + new Element('div.menus').adopt( + self.navigation_counter = new Element('span.counter[title=Total]'), + self.filter_menu = new Block.Menu(self, { + 'class': 'filter' + }), + self.navigation_actions = new Element('ul.actions', { + 'events': { + 'click:relay(li)': function(e, el){ + var a = 'active'; + self.navigation_actions.getElements('.'+a).removeClass(a); + self.changeView(el.get('data-view')); + this.addClass(a); + + el.inject(el.getParent(), 'top'); + el.getSiblings().hide(); + setTimeout(function(){ + el.getSiblings().setStyle('display', null); + }, 100) + } + } + }), + self.navigation_menu = new Block.Menu(self, { + 'class': 'extra' + }) + ) + ).inject(self.el, 'top'); + + // Mass edit + self.mass_edit_select_class = new Form.Check(self.mass_edit_select); + Quality.getActiveProfiles().each(function(profile){ + new Element('option', { + 'value': profile.get('_id'), + 'text': profile.get('label') + }).inject(self.mass_edit_quality) + }); + + self.filter_menu.addLink( + self.navigation_search_input = new Element('input', { + 'title': 'Search through ' + self.options.identifier, + 'placeholder': 'Search through ' + self.options.identifier, + 'events': { + 'keyup': self.search.bind(self), + 'change': self.search.bind(self) + } + }) + ).addClass('search'); + + var available_chars; + self.filter_menu.addEvent('open', function(){ + self.navigation_search_input.focus(); + + // Get available chars and highlight + if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) + Api.request('media.available_chars', { + 'data': Object.merge({ + 'status': self.options.status + }, self.filter), + 'onSuccess': function(json){ + available_chars = json.chars; + + available_chars.each(function(c){ + self.letters[c.capitalize()].addClass('available') + }) + + } + }); + }); + + self.filter_menu.addLink( + self.navigation_alpha = new Element('ul.numbers', { + 'events': { + 'click:relay(li.available)': function(e, el){ + self.activateLetter(el.get('data-letter')); + self.getMovies(true) + } + } + }) + ); + + // Actions + ['mass_edit', 'details', 'list'].each(function(view){ + var current = self.current_view == view; + new Element('li', { + 'class': 'icon2 ' + view + (current ? ' active ' : ''), + 'data-view': view + }).inject(self.navigation_actions, current ? 'top' : 'bottom'); + }); + + // All + self.letters['all'] = new Element('li.letter_all.available.active', { + 'text': 'ALL' + }).inject(self.navigation_alpha); + + // Chars + chars.split('').each(function(c){ + self.letters[c] = new Element('li', { + 'text': c, + 'class': 'letter_'+c, + 'data-letter': c + }).inject(self.navigation_alpha); + }); + + // Add menu or hide + if (self.options.menu.length > 0) + self.options.menu.each(function(menu_item){ + self.navigation_menu.addLink(menu_item); + }); + else + self.navigation_menu.hide(); + + }, + + calculateSelected: function(){ + var self = this; + + var selected = 0, + movies = self.movies.length; + self.movies.each(function(movie){ + selected += movie.isSelected() ? 1 : 0 + }); + + var indeterminate = selected > 0 && selected < movies, + checked = selected == movies && selected > 0; + + self.mass_edit_select.set('indeterminate', indeterminate); + + self.mass_edit_select_class[checked ? 'check' : 'uncheck'](); + self.mass_edit_select_class.element[indeterminate ? 'addClass' : 'removeClass']('indeterminate'); + + self.mass_edit_selected.set('text', selected); + }, + + deleteSelected: function(){ + var self = this, + ids = self.getSelectedMovies(), + help_msg = self.identifier == 'wanted' ? 'If you do, you won\'t be able to watch them, as they won\'t get downloaded!' : 'Your files will be safe, this will only delete the reference from the CouchPotato manage list'; + + var qObj = new Question('Are you sure you want to delete '+ids.length+' movie'+ (ids.length != 1 ? 's' : '') +'?', help_msg, [{ + 'text': 'Yes, delete '+(ids.length != 1 ? 'them' : 'it'), + 'class': 'delete', + 'events': { + 'click': function(e){ + (e).preventDefault(); + this.set('text', 'Deleting..'); + Api.request('media.delete', { + 'method': 'post', + 'data': { + 'id': ids.join(','), + 'delete_from': self.options.identifier + }, + 'onSuccess': function(){ + qObj.close(); + + var erase_movies = []; + self.movies.each(function(movie){ + if (movie.isSelected()){ + $(movie).destroy(); + erase_movies.include(movie); + } + }); + + erase_movies.each(function(movie){ + self.movies.erase(movie); + movie.destroy(); + self.setCounter(self.counter_count-1); + self.total_movies--; + }); + + self.calculateSelected(); + } + }); + + } + } + }, { + 'text': 'Cancel', + 'cancel': true + }]); + + }, + + changeQualitySelected: function(){ + var self = this; + var ids = self.getSelectedMovies(); + + Api.request('movie.edit', { + 'method': 'post', + 'data': { + 'id': ids.join(','), + 'profile_id': self.mass_edit_quality.get('value') + }, + 'onSuccess': self.search.bind(self) + }); + }, + + refreshSelected: function(){ + var self = this; + var ids = self.getSelectedMovies(); + + Api.request('media.refresh', { + 'method': 'post', + 'data': { + 'id': ids.join(',') + } + }); + }, + + getSelectedMovies: function(){ + var self = this; + + var ids = []; + self.movies.each(function(movie){ + if (movie.isSelected()) + ids.include(movie.get('_id')) + }); + + return ids + }, + + massEditToggleAll: function(){ + var self = this; + + var select = self.mass_edit_select.get('checked'); + + self.movies.each(function(movie){ + movie.select(select) + }); + + self.calculateSelected() + }, + + reset: function(){ + var self = this; + + self.movies = []; + if(self.mass_edit_select) + self.calculateSelected(); + if(self.navigation_alpha) + self.navigation_alpha.getElements('.active').removeClass('active'); + + self.offset = 0; + if(self.scrollspy){ + //self.load_more.show(); + self.scrollspy.start(); + } + }, + + activateLetter: function(letter){ + var self = this; + + self.reset(); + + self.letters[letter || 'all'].addClass('active'); + self.filter.starts_with = letter; + + }, + + changeView: function(new_view){ + var self = this; + + self.el + .removeClass(self.current_view+'_list') + .addClass(new_view+'_list'); + + self.current_view = new_view; + Cookie.write(self.options.identifier+'_view2', new_view, {duration: 1000}); + }, + + getSavedView: function(){ + var self = this; + return Cookie.read(self.options.identifier+'_view2'); + }, + + search: function(){ + var self = this; + + if(self.search_timer) clearTimeout(self.search_timer); + self.search_timer = (function(){ + var search_value = self.navigation_search_input.get('value'); + if (search_value == self.last_search_value) return; + + self.reset(); + + self.activateLetter(); + self.filter.search = search_value; + + self.getMovies(true); + + self.last_search_value = search_value; + + }).delay(250); + + }, + + update: function(){ + var self = this; + + self.reset(); + self.getMovies(true); + }, + + getMovies: function(reset){ + var self = this; + + if(self.scrollspy){ + self.scrollspy.stop(); + self.load_more.set('text', 'loading...'); + } + + if(self.movies.length == 0 && self.options.loader){ + + self.loader_first = new Element('div.loading').adopt( + new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'}) + ).inject(self.el, 'top'); + + createSpinner(self.loader_first, { + radius: 4, + length: 4, + width: 1 + }); + + self.el.setStyle('min-height', 93); + + } + + Api.request(self.options.api_call || 'media.list', { + 'data': Object.merge({ + 'type': self.options.type || 'movie', + 'status': self.options.status, + 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null + }, self.filter), + 'onSuccess': function(json){ + + if(reset) + self.movie_list.empty(); + + if(self.loader_first){ + var lf = self.loader_first; + self.loader_first.addClass('hide'); + self.loader_first = null; + setTimeout(function(){ + lf.destroy(); + }, 20000); + self.el.setStyle('min-height', null); + } + + self.store(json.movies); + self.addMovies(json.movies, json.total || json.movies.length); + if(self.scrollspy) { + self.load_more.set('text', 'load more movies'); + self.scrollspy.start(); + } + + self.checkIfEmpty(); + self.fireEvent('loaded'); + } + }); + }, + + loadMore: function(){ + var self = this; + if(self.offset >= self.options.limit) + self.getMovies() + }, + + store: function(movies){ + var self = this; + + self.offset += movies.length; + + }, + + checkIfEmpty: function(){ + var self = this; + + var is_empty = self.movies.length == 0 && (self.total_movies == 0 || self.total_movies === undefined); + + if(self.title) + self.title[is_empty ? 'hide' : 'show'](); + + if(self.description) + self.description.setStyle('display', [is_empty ? 'none' : '']); + + if(is_empty && self.options.on_empty_element){ + self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after'); + + if(self.navigation) + self.navigation.hide(); + + self.empty_element = self.options.on_empty_element; + } + else if(self.empty_element){ + self.empty_element.destroy(); + + if(self.navigation) + self.navigation.show(); + } + + }, + + toElement: function(){ + return this.el; + } + +}); diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css new file mode 100755 index 0000000..14dc462 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/show.css @@ -0,0 +1,1077 @@ +.shows { + padding: 10px 0 20px; + position: relative; + z-index: 3; + width: 100%; +} + + .shows > div { + clear: both; + } + + .shows > div .message { + display: block; + padding: 20px; + font-size: 20px; + color: white; + text-align: center; + } + .shows > div .message a { + padding: 20px; + display: block; + } + + .shows.thumbs_list > div:not(.description) { + margin-right: -4px; + } + + .shows .loading { + display: block; + padding: 20px 0 0 0; + width: 100%; + z-index: 3; + transition: all .4s cubic-bezier(0.9,0,0.1,1); + height: 40px; + opacity: 1; + position: absolute; + text-align: center; + } + .shows .loading.hide { + height: 0; + padding: 0; + opacity: 0; + margin-top: -20px; + overflow: hidden; + } + + .shows .loading .spinner { + display: inline-block; + } + + .shows .loading .message { + margin: 0 20px; + } + + .shows h2 { + margin-bottom: 20px; + } + + @media all and (max-width: 480px) { + .shows h2 { + font-size: 25px; + margin-bottom: 10px; + } + } + + .shows > .description { + position: absolute; + top: 30px; + right: 0; + font-style: italic; + opacity: 0.8; + } + .shows:hover > .description { + opacity: 1; + } + + @media all and (max-width: 860px) { + .shows > .description { + display: none; + } + } + + .shows.thumbs_list { + padding: 20px 0 20px; + } + + .home .shows { + padding-top: 6px; + } + + .shows .show { + position: relative; + margin: 10px 0; + padding-left: 20px; + overflow: hidden; + width: 100%; + height: 180px; + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); + transition-property: width, height; + background: rgba(0,0,0,.2); + } + + .shows .show.expanded { + height: 360px; + } + + .shows .show .table.expanded { + height: 360px; + } + + .shows.mass_edit_list .show { + padding-left: 22px; + background: none; + } + + .shows.details_list .show { + padding-left: 120px; + } + + .shows.list_list .show:not(.details_view), + .shows.mass_edit_list .show { + height: 30px; + border-bottom: 1px solid rgba(255,255,255,.15); + } + + .shows.list_list .show:last-child, + .shows.mass_edit_list .show:last-child { + border: none; + } + + .shows.thumbs_list .show { + width: 16.66667%; + height: auto; + min-height: 200px; + display: inline-block; + margin: 0; + padding: 0; + vertical-align: top; + line-height: 0; + } + + @media all and (max-width: 800px) { + .shows.thumbs_list .show { + width: 25%; + min-height: 100px; + } + } + + .shows .show .mask { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + } + + .shows.list_list .show:not(.details_view), + .shows.mass_edit_list .show { + margin: 0; + } + + .shows .data { + padding: 20px; + height: 100%; + width: 100%; + position: relative; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + right: 0; + } + .shows.list_list .show:not(.details_view) .data, + .shows.mass_edit_list .show .data { + padding: 0 0 0 10px; + border: 0; + background: #4e5969; + } + .shows.mass_edit_list .show .data { + padding-left: 8px; + } + + .shows.thumbs_list .data { + position: absolute; + left: 0; + top: 0; + width: 100%; + padding: 10px; + height: 100%; + background: none; + transition: none; + } + + .shows.thumbs_list .show:hover .data { + background: rgba(0,0,0,0.9); + } + + .shows .data.hide_right { + right: -100%; + } + + .shows .show .check { + display: none; + } + + .shows.mass_edit_list .show .check { + position: absolute; + left: 0; + top: 0; + display: block; + margin: 7px 0 0 5px; + } + + .shows .poster { + position: absolute; + left: 0; + width: 120px; + height: 180px; + line-height: 0; + overflow: hidden; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + background: rgba(0,0,0,.1); + } + .shows.thumbs_list .poster { + position: relative; + } + .shows.list_list .show:not(.details_view) .poster, + .shows.mass_edit_list .poster { + width: 20px; + height: 30px; + } + .shows.mass_edit_list .poster { + display: none; + } + + .shows.thumbs_list .poster { + width: 100%; + height: 100%; + transition: none; + background: no-repeat center; + background-size: cover; + } + .shows.thumbs_list .no_thumbnail .empty_file { + width: 100%; + height: 100%; + } + + .shows .poster img, + .options .poster img { + width: 100%; + height: 100%; + } + .shows.thumbs_list .poster img { + height: auto; + width: 100%; + top: 0; + bottom: 0; + opacity: 0; + } + + .shows .info { + position: relative; + height: 100%; + width: 100%; + } + + .shows .info .title { + font-size: 28px; + font-weight: bold; + margin-bottom: 10px; + margin-top: 2px; + width: 100%; + padding-right: 80px; + transition: all 0.2s linear; + height: 35px; + top: -5px; + position: relative; + } + .shows.list_list .info .title, + .shows.mass_edit_list .info .title { + height: 100%; + top: 0; + margin: 0; + } + .touch_enabled .shows.list_list .info .title { + display: inline-block; + padding-right: 55px; + } + + .shows .info .title a { + display: inline-block; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + width: 100%; + height: 100%; + line-height: 30px; + color: rgb(255, 255, 255); + } + + .shows .info .title a:hover { + color: rgba(255, 255, 255, 0.6); + } + + .shows.thumbs_list .info .title a { + white-space: normal; + overflow: auto; + height: auto; + text-align: left; + } + + @media all and (max-width: 480px) { + .shows.thumbs_list .show .info .title a, + .shows.thumbs_list .show .info .year { + font-size: 15px; + line-height: 15px; + overflow: hidden; + } + } + + .shows.list_list .show:not(.details_view) .info .title, + .shows.mass_edit_list .info .title { + font-size: 16px; + font-weight: normal; + width: auto; + } + + .shows.thumbs_list .show:not(.no_thumbnail) .info { + display: none; + } + .shows.thumbs_list .show:hover .info { + display: block; + } + + .shows.thumbs_list .info .title { + font-size: 21px; + word-wrap: break-word; + padding: 0; + height: 100%; + } + + .shows .info .year { + position: absolute; + color: #bbb; + right: 0; + top: 6px; + text-align: right; + transition: all 0.2s linear; + font-weight: normal; + } + .shows.list_list .show:not(.details_view) .info .year, + .shows.mass_edit_list .info .year { + font-size: 1.25em; + right: 10px; + } + + .shows.thumbs_list .info .year { + font-size: 23px; + margin: 0; + bottom: 0; + left: 0; + top: auto; + right: auto; + color: #FFF; + line-height: 18px; + } + + .touch_enabled .shows.list_list .show .info .year { + font-size: 1em; + } + + .shows .info .description { + top: 30px; + clear: both; + bottom: 30px; + position: absolute; + } + .shows.list_list .show:not(.details_view) .info .description, + .shows.mass_edit_list .info .description, + .shows.thumbs_list .info .description { + display: none; + } + + .shows .data .quality { + position: absolute; + bottom: 2px; + display: block; + min-height: 20px; + } + + .shows.list_list .show:hover .data .quality { + display: none; + } + + .touch_enabled .shows.list_list .show .data .quality { + position: relative; + display: inline-block; + margin: 0; + top: -4px; + } + + @media all and (max-width: 480px) { + .shows .data .quality { + display: none; + } + } + + .shows .status_suggest .data .quality, + .shows.thumbs_list .data .quality { + display: none; + } + + .shows .data .quality span { + padding: 2px 3px; + opacity: 0.5; + font-size: 10px; + height: 16px; + line-height: 12px; + vertical-align: middle; + display: inline-block; + text-transform: uppercase; + font-weight: normal; + margin: 0 4px 0 0; + border-radius: 2px; + background-color: rgba(255,255,255,0.1); + } + .shows.list_list .data .quality, + .shows.mass_edit_list .data .quality { + text-align: right; + right: 0; + margin-right: 60px; + z-index: 1; + top: 5px; + } + + .shows .data .quality .available, + .shows .data .quality .snatched, + .shows .data .quality .seeding { + opacity: 1; + cursor: pointer; + } + + .shows .data .quality .available { background-color: #578bc3; } + .shows .data .quality .failed, + .shows .data .quality .missing, + .shows .data .quality .ignored { background-color: #a43d34; } + .shows .data .quality .snatched { background-color: #a2a232; } + .shows .data .quality .done { + background-color: #369545; + opacity: 1; + } + .shows .data .quality .seeding { background-color: #0a6819; } + .shows .data .quality .finish { + background-image: url('../../images/sprite.png'); + background-repeat: no-repeat; + background-position: 0 2px; + padding-left: 14px; + background-size: 14px + } + + .shows .data .actions { + position: absolute; + bottom: 17px; + right: 20px; + line-height: 0; + top: 0; + width: auto; + opacity: 0; + display: none; + } + @media all and (max-width: 480px) { + .shows .data .actions { + display: none !important; + } + } + + .shows .show:hover .data .actions, + .touch_enabled .shows .show .data .actions { + opacity: 1; + display: inline-block; + } + + .shows.details_list .data .actions { + top: auto; + bottom: 18px; + } + + .shows .show:hover .actions { + opacity: 1; + display: inline-block; + } + .shows.thumbs_list .data .actions { + bottom: 12px; + right: 10px; + top: auto; + } + + .shows .show:hover .action { opacity: 0.6; } + .shows .show:hover .action:hover { opacity: 1; } + + .shows .data .action { + display: inline-block; + height: 22px; + min-width: 33px; + padding: 0 5px; + line-height: 26px; + text-align: center; + font-size: 13px; + color: #FFF; + margin-left: 1px; + } + .shows .data .action.trailer { color: #FFF; } + .shows .data .action.download { color: #b9dec0; } + .shows .data .action.edit { color: #c6b589; } + .shows .data .action.refresh { color: #cbeecc; } + .shows .data .action.delete { color: #e9b0b0; } + .shows .data .action.directory { color: #ffed92; } + .shows .data .action.readd { color: #c2fac5; } + + .shows.mass_edit_list .show .data .actions { + display: none; + } + + .shows.list_list .show:not(.details_view):hover .actions, + .shows.mass_edit_list .show:hover .actions, + .touch_enabled .shows.list_list .show:not(.details_view) .actions { + margin: 0; + background: #4e5969; + top: 2px; + bottom: 2px; + right: 5px; + z-index: 3; + } + + .shows .delete_container { + clear: both; + text-align: center; + font-size: 20px; + position: absolute; + padding: 80px 0 0; + left: 120px; + right: 0; + } + .shows .delete_container .or { + padding: 10px; + } + .shows .delete_container .delete { + background-color: #ff321c; + font-weight: normal; + } + .shows .delete_container .delete:hover { + color: #fff; + background-color: #d32917; + } + + .shows .options { + position: absolute; + right: 0; + left: 120px; + } + + .shows .options .form { + margin: 80px 0 0; + font-size: 20px; + text-align: center; + } + + .shows .options .form select { + margin-right: 20px; + } + + .shows .options .table { + height: 180px; + overflow: auto; + line-height: 2em; + } + .shows .options .table .item { + border-bottom: 1px solid rgba(255,255,255,0.1); + } + .shows .options .table .item.ignored span, + .shows .options .table .item.failed span { + text-decoration: line-through; + color: rgba(255,255,255,0.4); + } + .shows .options .table .item.ignored .delete:before, + .shows .options .table .item.failed .delete:before { + display: inline-block; + content: "\e04b"; + transform: scale(-1, 1); + } + + .shows .options .table .item:last-child { border: 0; } + .shows .options .table .item:nth-child(even) { + background: rgba(255,255,255,0.05); + } + .shows .options .table .item:not(.head):hover { + background: rgba(255,255,255,0.03); + } + + .shows .options .table .item > * { + display: inline-block; + padding: 0 5px; + width: 60px; + min-height: 24px; + white-space: nowrap; + text-overflow: ellipsis; + text-align: center; + vertical-align: top; + border-left: 1px solid rgba(255, 255, 255, 0.1); + } + .shows .options .table .item > *:first-child { + border: 0; + } + .shows .options .table .provider { + width: 120px; + text-overflow: ellipsis; + overflow: hidden; + } + .shows .options .table .name { + width: 340px; + overflow: hidden; + text-align: left; + padding: 0 10px; + } + .shows .options .table.files .name { width: 590px; } + .shows .options .table .type { width: 130px; } + .shows .options .table .is_available { width: 90px; } + .shows .options .table .age, + .shows .options .table .size { width: 40px; } + + .shows .options .table a { + width: 30px !important; + height: 20px; + opacity: 0.8; + line-height: 25px; + } + .shows .options .table a:hover { opacity: 1; } + .shows .options .table a.download { color: #a7fbaf; } + .shows .options .table a.delete { color: #fda3a3; } + .shows .options .table .ignored a.delete, + .shows .options .table .failed a.delete { color: #b5fda3; } + + .shows .options .table .head > * { + font-weight: bold; + font-size: 14px; + padding-top: 4px; + padding-bottom: 4px; + height: auto; + } + + .trailer_container { + width: 100%; + background: #000; + text-align: center; + transition: all .6s cubic-bezier(0.9,0,0.1,1); + overflow: hidden; + left: 0; + position: absolute; + z-index: 10; + } + @media only screen and (device-width: 768px) { + .trailer_container iframe { + margin-top: 25px; + } + } + + .trailer_container.hide { + height: 0 !important; + } + + .hide_trailer { + position: absolute; + top: 0; + left: 50%; + margin-left: -50px; + width: 100px; + text-align: center; + padding: 3px 10px; + background: #4e5969; + transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; + z-index: 11; + } + .hide_trailer.hide { + top: -30px; + } + + .shows .show .episodes .item { + position: relative; + } + + .shows .show .episodes .actions { + position: absolute; + width: auto; + right: 0; + + border-left: none; + } + + .shows .show .episodes .actions .refresh { + color: #cbeecc; + } + + .shows .show .try_container { + padding: 5px 10px; + text-align: center; + } + + .shows .show .try_container a { + margin: 0 5px; + padding: 2px 5px; + } + + .shows .show .releases .next_release { + border-left: 6px solid #2aa300; + } + + .shows .show .releases .next_release > :first-child { + margin-left: -6px; + } + + .shows .show .releases .last_release { + border-left: 6px solid #ffa200; + } + + .shows .show .releases .last_release > :first-child { + margin-left: -6px; + } + .shows .show .trynext { + display: inline; + position: absolute; + right: 180px; + z-index: 2; + opacity: 0; + background: #4e5969; + text-align: right; + height: 100%; + top: 0; + } + .touch_enabled .shows .show .trynext { + display: none; + } + + @media all and (max-width: 480px) { + .shows .show .trynext { + display: none; + } + } + .shows.mass_edit_list .trynext { display: none; } + .wanted .shows .show .trynext { + padding-right: 30px; + } + .shows .show:hover .trynext, + .touch_enabled .shows.details_list .show .trynext { + opacity: 1; + } + + .shows.details_list .show .trynext { + background: #47515f; + padding: 0; + right: 0; + height: 25px; + } + + .shows .show .trynext a { + background-position: 5px center; + padding: 0 5px 0 25px; + margin-right: 10px; + color: #FFF; + height: 100%; + line-height: 27px; + font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; + } + .shows .show .trynext a:before { + margin: 2px 0 0 -20px; + position: absolute; + font-family: 'Elusive-Icons'; + } + .shows.details_list .show .trynext a { + line-height: 23px; + } + .shows .show .trynext a:last-child { + margin: 0; + } + .shows .show .trynext a:hover, + .touch_enabled .shows .show .trynext a { + background-color: #369545; + } + + .shows .load_more { + display: block; + padding: 10px; + text-align: center; + font-size: 20px; + } + .shows .load_more.loading { + opacity: .5; + } + +.shows .alph_nav { + height: 44px; +} + + @media all and (max-width: 480px) { + .shows .alph_nav { + display: none; + } + } + + .shows .alph_nav .menus { + display: inline-block; + float: right; + } + +.shows .alph_nav .numbers, +.shows .alph_nav .counter, +.shows .alph_nav .actions { + list-style: none; + padding: 0 0 1px; + margin: 0; + user-select: none; +} + + .shows .alph_nav .counter { + display: inline-block; + text-align: right; + padding: 0 10px; + height: 100%; + line-height: 43px; + border-right: 1px solid rgba(255,255,255,.07); + } + + .shows .alph_nav .numbers li, + .shows .alph_nav .actions li { + display: inline-block; + vertical-align: top; + height: 100%; + line-height: 30px; + text-align: center; + border: 1px solid transparent; + transition: all 0.1s ease-in-out; + } + + .shows .alph_nav .numbers li { + width: 30px; + height: 30px; + opacity: 0.3; + } + .shows .alph_nav .numbers li.letter_all { + width: 60px; + } + + .shows .alph_nav li.available { + font-weight: bold; + cursor: pointer; + opacity: 1; + + } + .shows .alph_nav li.active.available, + .shows .alph_nav li.available:hover { + background: rgba(0,0,0,.1); + } + + .shows .alph_nav .search input { + width: 100%; + height: 44px; + display: inline-block; + border: 0; + background: none; + color: #444; + font-size: 14px; + padding: 0 10px 0 30px; + border-bottom: 1px solid rgba(0,0,0,.08); + } + .shows .alph_nav .search input:focus { + background: rgba(0,0,0,.08); + } + + .shows .alph_nav .search input::-webkit-input-placeholder { + color: #444; + opacity: .6; + } + + .shows .alph_nav .search:before { + font-family: 'Elusive-Icons'; + content: "\e03e"; + position: absolute; + height: 20px; + line-height: 45px; + font-size: 12px; + margin: 0 0 0 10px; + opacity: .6; + color: #444; + } + + .shows .alph_nav .actions { + -moz-user-select: none; + width: 44px; + height: 44px; + display: inline-block; + vertical-align: top; + z-index: 200; + position: relative; + border: 1px solid rgba(255,255,255,.07); + border-width: 0 1px; + } + .shows .alph_nav .actions:hover { + box-shadow: 0 100px 20px -10px rgba(0,0,0,0.55); + } + .shows .alph_nav .actions li { + width: 100%; + height: 45px; + line-height: 40px; + position: relative; + z-index: 20; + display: none; + cursor: pointer; + } + .shows .alph_nav .actions:hover li:not(.active) { + display: block; + background: #FFF; + color: #444; + } + .shows .alph_nav .actions li:hover:not(.active) { + background: #ccc; + } + .shows .alph_nav .actions li.active { + display: block; + } + + .shows .alph_nav .actions li.mass_edit:before { + content: "\e070"; + } + + .shows .alph_nav .actions li.list:before { + content: "\e0d8"; + } + + .shows .alph_nav .actions li.details:before { + content: "\e022"; + } + + .shows .alph_nav .mass_edit_form { + clear: both; + text-align: center; + display: none; + overflow: hidden; + float: left; + height: 44px; + line-height: 44px; + } + .shows.mass_edit_list .mass_edit_form { + display: inline-block; + } + .shows.mass_edit_list .mass_edit_form .select { + font-size: 14px; + display: inline-block; + } + .shows.mass_edit_list .mass_edit_form .select .check { + display: inline-block; + vertical-align: middle; + margin: -4px 0 0 5px; + } + .shows.mass_edit_list .mass_edit_form .select span { + opacity: 0.7; + } + .shows.mass_edit_list .mass_edit_form .select .count { + font-weight: bold; + margin: 0 3px 0 10px; + } + + .shows .alph_nav .mass_edit_form .quality { + display: inline-block; + margin: 0 0 0 16px; + } + .shows .alph_nav .mass_edit_form .quality select { + width: 120px; + margin-right: 5px; + } + .shows .alph_nav .mass_edit_form .button { + padding: 3px 7px; + } + + .shows .alph_nav .mass_edit_form .refresh, + .shows .alph_nav .mass_edit_form .delete { + display: inline-block; + margin-left: 8px; + } + + .shows .alph_nav .mass_edit_form .refresh span, + .shows .alph_nav .mass_edit_form .delete span { + margin: 0 10px 0 0; + } + + .shows .alph_nav .more_menu > a { + background: none; + } + + .shows .alph_nav .more_menu.extra > a:before { + content: '...'; + font-size: 1.7em; + line-height: 23px; + text-align: center; + display: block; + } + + .shows .alph_nav .more_menu.filter { + } + + .shows .alph_nav .more_menu.filter > a:before { + content: "\e0e8"; + font-family: 'Elusive-Icons'; + line-height: 33px; + display: block; + text-align: center; + } + + .shows .alph_nav .more_menu.filter .wrapper { + right: 88px; + width: 300px; + } + +.shows .empty_wanted { + background-image: url('../../images/emptylist.png'); + background-position: 80% 0; + height: 750px; + width: 100%; + max-width: 900px; + padding-top: 260px; +} + +.shows .empty_manage { + text-align: center; + font-size: 25px; + line-height: 150%; + padding: 40px 0; +} + + .shows .empty_manage .after_manage { + margin-top: 30px; + font-size: 16px; + } + + .shows .progress { + padding: 10px; + margin: 5px 0; + text-align: left; + } + + .shows .progress > div { + padding: 5px 10px; + font-size: 12px; + line-height: 12px; + text-align: left; + display: inline-block; + width: 49%; + background: rgba(255, 255, 255, 0.05); + margin: 2px 0.5%; + } + + .shows .progress > div .folder { + display: inline-block; + padding: 5px 20px 5px 0; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + width: 85%; + direction: ltr; + vertical-align: middle; + } + + .shows .progress > div .percentage { + display: inline-block; + text-transform: uppercase; + font-weight: normal; + font-size: 20px; + border-left: 1px solid rgba(255, 255, 255, .2); + width: 15%; + text-align: right; + vertical-align: middle; + } diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js new file mode 100755 index 0000000..b49063e --- /dev/null +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -0,0 +1,109 @@ +var Episodes = new Class({ + initialize: function(show) { + var self = this; + + self.show = show; + }, + + open: function(){ + var self = this; + + if(!self.container){ + self.container = new Element('div.options').grab( + self.episodes_container = new Element('div.episodes.table') + ); + + self.container.inject(self.show, 'top'); + + Api.request('library.tree', { + 'data': { + 'media_id': self.show.data._id + }, + 'onComplete': function(json){ + self.data = json.result; + + self.createEpisodes(); + } + }); + } + + self.show.slide('in', self.container); + }, + + createEpisodes: function() { + var self = this; + + self.data.seasons.sort(function(a, b) { + var an = a.info.number || 0; + var bn = b.info.number || 0; + + if(an < bn) + return -1; + + if(an > bn) + return 1; + + return 0; + }); + + self.data.seasons.each(function(season) { + season['el'] = new Element('div', { + 'class': 'item head', + 'id': 'season_'+season._id + }).adopt( + new Element('span.name', {'text': 'Season ' + (season.info.number || 0)}) + ).inject(self.episodes_container); + + season.episodes.sort(function(a, b) { + var an = a.info.number || 0; + var bn = b.info.number || 0; + + if(an < bn) + return -1; + + if(an > bn) + return 1; + + return 0; + }); + + season.episodes.each(function(episode) { + var title = ''; + + if(episode.info.titles && episode.info.titles.length > 0) { + title = episode.info.titles[0]; + } + + episode['el'] = new Element('div', { + 'class': 'item', + 'id': 'episode_'+episode._id + }).adopt( + new Element('span.episode', {'text': (episode.info.number || 0)}), + new Element('span.name', {'text': title}), + new Element('span.firstaired', {'text': episode.info.firstaired}) + ).inject(self.episodes_container); + + episode['el_actions'] = new Element('div.actions').inject(episode['el']); + + if(episode.identifiers && episode.identifiers.imdb) { + new Element('a.imdb.icon2', { + 'title': 'Go to the IMDB page of ' + self.show.getTitle(), + 'href': 'http://www.imdb.com/title/' + episode.identifiers.imdb + '/', + 'target': '_blank' + }).inject(episode['el_actions']); + } + + new Element('a.refresh.icon2', { + 'title': 'Refresh the episode info and do a forced search', + 'events': { + 'click': self.doRefresh.bind(self) + } + }).inject(episode['el_actions']); + }); + }); + }, + + doRefresh: function() { + + } +}); \ No newline at end of file diff --git a/couchpotato/core/media/show/_base/static/show.js b/couchpotato/core/media/show/_base/static/show.js new file mode 100755 index 0000000..b91e637 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/show.js @@ -0,0 +1,347 @@ +var Show = new Class({ + + Extends: BlockBase, + + action: {}, + + initialize: function(list, options, data){ + var self = this; + + self.data = data; + self.view = options.view || 'details'; + self.list = list; + + self.el = new Element('div.show'); + + self.episodes = new Episodes(self); + + self.profile = Quality.getProfile(data.profile_id) || {}; + self.category = CategoryList.getCategory(data.category_id) || {}; + self.parent(self, options); + + self.addEvents(); + }, + + addEvents: function(){ + var self = this; + + self.global_events = {}; + + // Do refresh with new data + self.global_events['movie.update'] = function(notification){ + if(self.data._id != notification.data._id) return; + + self.busy(false); + self.removeView(); + self.update.delay(2000, self, notification); + }; + App.on('movie.update', self.global_events['movie.update']); + + // Add spinner on load / search + ['media.busy', 'movie.searcher.started'].each(function(listener){ + self.global_events[listener] = function(notification){ + if(notification.data && (self.data._id == notification.data._id || (typeOf(notification.data._id) == 'array' && notification.data._id.indexOf(self.data._id) > -1))) + self.busy(true); + }; + App.on(listener, self.global_events[listener]); + }); + + // Remove spinner + self.global_events['movie.searcher.ended'] = function(notification){ + if(notification.data && self.data._id == notification.data._id) + self.busy(false) + }; + App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']); + + // Reload when releases have updated + self.global_events['release.update_status'] = function(notification){ + var data = notification.data; + if(data && self.data._id == data.movie_id){ + + if(!self.data.releases) + self.data.releases = []; + + self.data.releases.push({'quality': data.quality, 'status': data.status}); + self.updateReleases(); + } + }; + + App.on('release.update_status', self.global_events['release.update_status']); + + }, + + destroy: function(){ + var self = this; + + self.el.destroy(); + delete self.list.movies_added[self.get('id')]; + self.list.movies.erase(self); + + self.list.checkIfEmpty(); + + // Remove events + Object.each(self.global_events, function(handle, listener){ + App.off(listener, handle); + }); + }, + + busy: function(set_busy, timeout){ + var self = this; + + if(!set_busy){ + setTimeout(function(){ + if(self.spinner){ + self.mask.fade('out'); + setTimeout(function(){ + if(self.mask) + self.mask.destroy(); + if(self.spinner) + self.spinner.el.destroy(); + self.spinner = null; + self.mask = null; + }, timeout || 400); + } + }, timeout || 1000) + } + else if(!self.spinner) { + self.createMask(); + self.spinner = createSpinner(self.mask); + self.mask.fade('in'); + } + }, + + createMask: function(){ + var self = this; + self.mask = new Element('div.mask', { + 'styles': { + 'z-index': 4 + } + }).inject(self.el, 'top').fade('hide'); + }, + + update: function(notification){ + var self = this; + + self.data = notification.data; + self.el.empty(); + self.removeView(); + + self.profile = Quality.getProfile(self.data.profile_id) || {}; + self.category = CategoryList.getCategory(self.data.category_id) || {}; + self.create(); + + self.busy(false); + }, + + create: function(){ + var self = this; + + self.el.addClass('status_'+self.get('status')); + + self.el.adopt( + self.select_checkbox = new Element('input[type=checkbox].inlay', { + 'events': { + 'change': function(){ + self.fireEvent('select') + } + } + }), + self.thumbnail = (self.data.files && self.data.files.image_poster) ? new Element('img', { + 'class': 'type_image poster', + 'src': Api.createUrl('file.cache') + self.data.files.image_poster[0].split(Api.getOption('path_sep')).pop() + }): null, + self.data_container = new Element('div.data.inlay.light').adopt( + self.info_container = new Element('div.info').adopt( + new Element('div.title').adopt( + self.title = new Element('a', { + 'events': { + 'click': function(e){ + self.episodes.open(); + } + }, + 'text': self.getTitle() || 'n/a' + }), + self.year = new Element('div.year', { + 'text': self.data.info.year || 'n/a' + }) + ), + self.description = new Element('div.description.tiny_scroll', { + 'text': self.data.info.plot + }), + self.quality = new Element('div.quality', { + 'events': { + 'click': function(e){ + var releases = self.el.getElement('.actions .releases'); + if(releases.isVisible()) + releases.fireEvent('click', [e]) + } + } + }) + ), + self.actions = new Element('div.actions') + ) + ); + + if(!self.thumbnail) + self.el.addClass('no_thumbnail'); + + //self.changeView(self.view); + self.select_checkbox_class = new Form.Check(self.select_checkbox); + + // Add profile + if(self.profile.data) + self.profile.getTypes().each(function(type){ + + var q = self.addQuality(type.get('quality'), type.get('3d')); + if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ + q.addClass('finish'); + q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') + } + + }); + + // Add releases + self.updateReleases(); + + Object.each(self.options.actions, function(action, key){ + self.action[key.toLowerCase()] = action = new self.options.actions[key](self); + if(action.el) + self.actions.adopt(action) + }); + + }, + + updateReleases: function(){ + var self = this; + if(!self.data.releases || self.data.releases.length == 0) return; + + self.data.releases.each(function(release){ + + var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')), + status = release.status; + + if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) + q = self.addQuality(release.quality, release.is_3d || false); + + if (q && !q.hasClass(status)){ + q.addClass(status); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status) + } + + }); + }, + + addQuality: function(quality, is_3d){ + var self = this; + + var q = Quality.getQuality(quality); + return new Element('span', { + 'text': q.label + (is_3d ? ' 3D' : ''), + 'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''), + 'title': '' + }).inject(self.quality); + + }, + + getTitle: function(){ + var self = this; + + if(self.data.title) + return self.getUnprefixedTitle(self.data.title); + else if(self.data.info.titles.length > 0) + return self.getUnprefixedTitle(self.data.info.titles[0]); + + return 'Unknown movie' + }, + + getUnprefixedTitle: function(t){ + if(t.substr(0, 4).toLowerCase() == 'the ') + t = t.substr(4) + ', The'; + else if(t.substr(0, 3).toLowerCase() == 'an ') + t = t.substr(3) + ', An'; + else if(t.substr(0, 2).toLowerCase() == 'a ') + t = t.substr(2) + ', A'; + return t; + }, + + slide: function(direction, el){ + var self = this; + + if(direction == 'in'){ + self.temp_view = self.view; + self.changeView('details'); + + self.el.addEvent('outerClick', function(){ + self.removeView(); + self.slide('out') + }); + el.show(); + + + self.el.addClass('expanded'); + self.el.getElements('.table').addClass('expanded'); + + self.data_container.addClass('hide_right'); + } + else { + self.el.removeEvents('outerClick'); + + setTimeout(function(){ + if(self.el) + { + self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide(); + self.el.getElements('.table').removeClass('expanded'); + } + }, 600); + + self.el.removeClass('expanded'); + self.data_container.removeClass('hide_right'); + } + }, + + changeView: function(new_view){ + var self = this; + + if(self.el) + self.el + .removeClass(self.view+'_view') + .addClass(new_view+'_view'); + + self.view = new_view; + }, + + removeView: function(){ + var self = this; + + self.el.removeClass(self.view+'_view') + }, + + getIdentifier: function(){ + var self = this; + + try { + return self.get('identifiers').imdb; + } + catch (e){ } + + return self.get('imdb'); + }, + + get: function(attr){ + return this.data[attr] || this.data.info[attr] + }, + + select: function(bool){ + var self = this; + self.select_checkbox_class[bool ? 'check' : 'uncheck']() + }, + + isSelected: function(){ + return this.select_checkbox.get('checked'); + }, + + toElement: function(){ + return this.el; + } + +}); From c81891683cc443b43653960c02c5e8b5a360f028 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Fri, 25 Jul 2014 17:12:42 +1200 Subject: [PATCH 10/59] [TV] Cleaner season/episode titles in list, move specials to bottom --- .../core/media/show/_base/static/show.episodes.js | 36 ++++++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js index b49063e..e5b9968 100755 --- a/couchpotato/core/media/show/_base/static/show.episodes.js +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -34,34 +34,48 @@ var Episodes = new Class({ var self = this; self.data.seasons.sort(function(a, b) { - var an = a.info.number || 0; - var bn = b.info.number || 0; + // Move "Specials" to the bottom of the list + if(!a.info.number) { + return 1; + } + + if(!b.info.number) { + return -1; + } - if(an < bn) + // Order seasons descending + if(a.info.number < b.info.number) return -1; - if(an > bn) + if(a.info.number > b.info.number) return 1; return 0; }); self.data.seasons.each(function(season) { + var title = ''; + + if(season.info.number) { + title = 'Season ' + season.info.number; + } else { + // Season 0 / Specials + title = 'Specials'; + } + season['el'] = new Element('div', { 'class': 'item head', 'id': 'season_'+season._id }).adopt( - new Element('span.name', {'text': 'Season ' + (season.info.number || 0)}) + new Element('span.name', {'text': title}) ).inject(self.episodes_container); season.episodes.sort(function(a, b) { - var an = a.info.number || 0; - var bn = b.info.number || 0; - - if(an < bn) + // Order episodes descending + if(a.info.number < b.info.number) return -1; - if(an > bn) + if(a.info.number > b.info.number) return 1; return 0; @@ -72,6 +86,8 @@ var Episodes = new Class({ if(episode.info.titles && episode.info.titles.length > 0) { title = episode.info.titles[0]; + } else { + title = 'Episode ' + episode.info.number; } episode['el'] = new Element('div', { From c038c66dc9a127eb134440570bfb8cd24e94298c Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 13:54:55 +1200 Subject: [PATCH 11/59] Switched "library.tree" to use "media_children" index --- couchpotato/core/media/_base/library/main.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index d20342f..c0e4464 100755 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -80,18 +80,12 @@ class Library(LibraryBase): result = media db = get_db() - - # TODO this probably should be using an index? - items = [ - item['doc'] - for item in db.all('media', with_doc = True) - if item['doc'].get('parent_id') == media['_id'] - ] + items = db.get_many('media_children', media['_id'], with_doc = True) keys = [] for item in items: - key = item['type'] + 's' + key = item['doc']['type'] + 's' if key not in result: result[key] = {} @@ -99,7 +93,7 @@ class Library(LibraryBase): if key not in keys: keys.append(key) - result[key][item['_id']] = fireEvent('library.tree', item, single = True) + result[key][item['_id']] = fireEvent('library.tree', item['doc'], single = True) for key in keys: result[key] = result[key].values() From ac857301ace633c52b78a1a43b89716a1f8f6b02 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 14:23:20 +1200 Subject: [PATCH 12/59] [TV] Create "Episode" class, "media.refresh" is now fired --- .../core/media/show/_base/static/episode.js | 71 +++++++++++ .../core/media/show/_base/static/show.episodes.js | 130 +++++++++------------ 2 files changed, 125 insertions(+), 76 deletions(-) create mode 100755 couchpotato/core/media/show/_base/static/episode.js diff --git a/couchpotato/core/media/show/_base/static/episode.js b/couchpotato/core/media/show/_base/static/episode.js new file mode 100755 index 0000000..2ba18a9 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/episode.js @@ -0,0 +1,71 @@ +var Episode = new Class({ + + Extends: BlockBase, + + action: {}, + + initialize: function(show, data){ + var self = this; + + self.show = show; + self.data = data; + + self.el = new Element('div.item'); + self.el_actions = new Element('div.actions'); + + self.create(); + }, + + create: function(){ + var self = this; + + self.el.set('id', 'episode_'+self.data._id); + + self.el.adopt( + new Element('span.episode', {'text': (self.data.info.number || 0)}), + new Element('span.name', {'text': self.getTitle()}), + new Element('span.firstaired', {'text': self.data.info.firstaired}) + ); + + self.el_actions.inject(self.el); + + if(self.data.identifiers && self.data.identifiers.imdb) { + new Element('a.imdb.icon2', { + 'title': 'Go to the IMDB page of ' + self.show.getTitle(), + 'href': 'http://www.imdb.com/title/' + self.data.identifiers.imdb + '/', + 'target': '_blank' + }).inject(self.el_actions); + } + + new Element('a.refresh.icon2', { + 'title': 'Refresh the episode info and do a forced search', + 'events': { + 'click': self.doRefresh.bind(self) + } + }).inject(self.el_actions); + }, + + getTitle: function(){ + var self = this; + + var title = ''; + + if(self.data.info.titles && self.data.info.titles.length > 0) { + title = self.data.info.titles[0]; + } else { + title = 'Episode ' + self.data.info.number; + } + + return title; + }, + + doRefresh: function(e) { + var self = this; + + Api.request('media.refresh', { + 'data': { + 'id': self.data._id + } + }); + } +}); \ No newline at end of file diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js index e5b9968..ace93af 100755 --- a/couchpotato/core/media/show/_base/static/show.episodes.js +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -33,93 +33,71 @@ var Episodes = new Class({ createEpisodes: function() { var self = this; - self.data.seasons.sort(function(a, b) { - // Move "Specials" to the bottom of the list - if(!a.info.number) { - return 1; - } + self.data.seasons.sort(self.sortSeasons); + self.data.seasons.each(function(season) { + self.createSeason(season); - if(!b.info.number) { - return -1; - } + season.episodes.sort(self.sortEpisodes); + season.episodes.each(function(episode) { + self.createEpisode(episode); + }); + }); + }, - // Order seasons descending - if(a.info.number < b.info.number) - return -1; + createSeason: function(season) { + var self = this, + title = ''; - if(a.info.number > b.info.number) - return 1; + if(season.info.number) { + title = 'Season ' + season.info.number; + } else { + // Season 0 / Specials + title = 'Specials'; + } - return 0; - }); + season['el'] = new Element('div', { + 'class': 'item head', + 'id': 'season_'+season._id + }).adopt( + new Element('span.name', {'text': title}) + ).inject(self.episodes_container); + }, - self.data.seasons.each(function(season) { - var title = ''; - - if(season.info.number) { - title = 'Season ' + season.info.number; - } else { - // Season 0 / Specials - title = 'Specials'; - } - - season['el'] = new Element('div', { - 'class': 'item head', - 'id': 'season_'+season._id - }).adopt( - new Element('span.name', {'text': title}) - ).inject(self.episodes_container); - - season.episodes.sort(function(a, b) { - // Order episodes descending - if(a.info.number < b.info.number) - return -1; - - if(a.info.number > b.info.number) - return 1; - - return 0; - }); + createEpisode: function(episode){ + var self = this, + e = new Episode(self.show, episode); - season.episodes.each(function(episode) { - var title = ''; + $(e).inject(self.episodes_container); + }, - if(episode.info.titles && episode.info.titles.length > 0) { - title = episode.info.titles[0]; - } else { - title = 'Episode ' + episode.info.number; - } + sortSeasons: function(a, b) { + // Move "Specials" to the bottom of the list + if(!a.info.number) { + return 1; + } - episode['el'] = new Element('div', { - 'class': 'item', - 'id': 'episode_'+episode._id - }).adopt( - new Element('span.episode', {'text': (episode.info.number || 0)}), - new Element('span.name', {'text': title}), - new Element('span.firstaired', {'text': episode.info.firstaired}) - ).inject(self.episodes_container); - - episode['el_actions'] = new Element('div.actions').inject(episode['el']); - - if(episode.identifiers && episode.identifiers.imdb) { - new Element('a.imdb.icon2', { - 'title': 'Go to the IMDB page of ' + self.show.getTitle(), - 'href': 'http://www.imdb.com/title/' + episode.identifiers.imdb + '/', - 'target': '_blank' - }).inject(episode['el_actions']); - } + if(!b.info.number) { + return -1; + } - new Element('a.refresh.icon2', { - 'title': 'Refresh the episode info and do a forced search', - 'events': { - 'click': self.doRefresh.bind(self) - } - }).inject(episode['el_actions']); - }); - }); + // Order seasons descending + if(a.info.number < b.info.number) + return -1; + + if(a.info.number > b.info.number) + return 1; + + return 0; }, - doRefresh: function() { + sortEpisodes: function(a, b) { + // Order episodes descending + if(a.info.number < b.info.number) + return -1; + + if(a.info.number > b.info.number) + return 1; + return 0; } }); \ No newline at end of file From 364527b0b2a66aa612a00149aded79ae6ff601c4 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 22:29:16 +1200 Subject: [PATCH 13/59] Fixed "library.related" and "libary.tree" to work with "show.episode", 'show.season" media types --- couchpotato/core/media/_base/library/main.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index c0e4464..b565d16 100755 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -63,7 +63,9 @@ class Library(LibraryBase): while cur and cur.get('parent_id'): cur = db.get('id', cur['parent_id']) - result[cur['type']] = cur + parts = cur['type'].split('.') + + result[parts[-1]] = cur return result @@ -85,7 +87,8 @@ class Library(LibraryBase): keys = [] for item in items: - key = item['doc']['type'] + 's' + parts = item['doc']['type'].split('.') + key = parts[-1] + 's' if key not in result: result[key] = {} From 0f434afd335fbe65ead8f9bf23fd6f4c15814c87 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 22:30:48 +1200 Subject: [PATCH 14/59] [TV] Prefix child media types with "show." --- couchpotato/core/media/show/episode.py | 2 +- couchpotato/core/media/show/library/episode.py | 4 ++-- couchpotato/core/media/show/library/season.py | 4 ++-- couchpotato/core/media/show/searcher/episode.py | 2 +- couchpotato/core/media/show/season.py | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/episode.py mode change 100644 => 100755 couchpotato/core/media/show/library/episode.py mode change 100644 => 100755 couchpotato/core/media/show/library/season.py mode change 100644 => 100755 couchpotato/core/media/show/searcher/episode.py mode change 100644 => 100755 couchpotato/core/media/show/season.py diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py old mode 100644 new mode 100755 index 0f05f28..216b7ab --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -26,7 +26,7 @@ class Episode(MediaBase): # Add Season episode_info = { '_t': 'media', - 'type': 'episode', + 'type': 'show.episode', 'identifiers': identifiers, 'parent_id': parent_id, 'info': info, # Returned dict by providers diff --git a/couchpotato/core/media/show/library/episode.py b/couchpotato/core/media/show/library/episode.py old mode 100644 new mode 100755 index 7161f2d..26b5c3d --- a/couchpotato/core/media/show/library/episode.py +++ b/couchpotato/core/media/show/library/episode.py @@ -14,7 +14,7 @@ class EpisodeLibraryPlugin(LibraryBase): addEvent('library.identifier', self.identifier) def query(self, media, first = True, condense = True, include_identifier = True, **kwargs): - if media.get('type') != 'episode': + if media.get('type') != 'show.episode': return related = fireEvent('library.related', media, single = True) @@ -43,7 +43,7 @@ class EpisodeLibraryPlugin(LibraryBase): return titles def identifier(self, media): - if media.get('type') != 'episode': + if media.get('type') != 'show.episode': return identifier = { diff --git a/couchpotato/core/media/show/library/season.py b/couchpotato/core/media/show/library/season.py old mode 100644 new mode 100755 index 228d143..89489af --- a/couchpotato/core/media/show/library/season.py +++ b/couchpotato/core/media/show/library/season.py @@ -14,7 +14,7 @@ class SeasonLibraryPlugin(LibraryBase): addEvent('library.identifier', self.identifier) def query(self, media, first = True, condense = True, include_identifier = True, **kwargs): - if media.get('type') != 'season': + if media.get('type') != 'show.season': return related = fireEvent('library.related', media, single = True) @@ -44,7 +44,7 @@ class SeasonLibraryPlugin(LibraryBase): return titles def identifier(self, media): - if media.get('type') != 'season': + if media.get('type') != 'show.season': return return { diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py old mode 100644 new mode 100755 index 1e6b253..ace9768 --- a/couchpotato/core/media/show/searcher/episode.py +++ b/couchpotato/core/media/show/searcher/episode.py @@ -116,7 +116,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): log.info2('Too early to search for %s, %s', (too_early_to_search, query)) def correctRelease(self, release = None, media = None, quality = None, **kwargs): - if media.get('type') != 'episode': + if media.get('type') != 'show.episode': return retention = Env.setting('retention', section = 'nzb') diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py old mode 100644 new mode 100755 index a2dde78..6197c9c --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -28,7 +28,7 @@ class Season(MediaBase): # Add Season season_info = { '_t': 'media', - 'type': 'season', + 'type': 'show.season', 'identifiers': identifiers, 'parent_id': parent_id, 'info': info, # Returned dict by providers From 5f2dd0aac303da64c11f69967ea48df820101a28 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 22:31:58 +1200 Subject: [PATCH 15/59] [TV] Fixed episode info updates --- couchpotato/core/media/show/episode.py | 11 ++++++++++- couchpotato/core/media/show/providers/info/thetvdb.py | 17 ++++++++++++----- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index 216b7ab..e184e90 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -63,7 +63,16 @@ class Episode(MediaBase): # Get new info if not info: - info = fireEvent('episode.info', episode.get('identifiers'), merge = True) + season = db.get('id', episode['parent_id']) + show = db.get('id', season['parent_id']) + + info = fireEvent( + 'episode.info', show.get('identifiers'), { + 'season_identifier': season.get('info', {}).get('number'), + 'episode_identifier': episode.get('identifiers') + }, + merge = True + ) # Update/create media if force: diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index b2e58d9..e57057c 100755 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -172,15 +172,16 @@ class TheTVDb(ShowProvider): """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) + season_identifier = self.getIdentifier(params.get('season_identifier', None)) + episode_identifier = self.getIdentifier(params.get('episode_identifier', None)) + identifier = self.getIdentifier(identifier) if not identifier and season_identifier is None: return False # 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: + if not identifier and season_identifier: try: identifier, season_identifier = season_identifier.split(':') season_identifier = int(season_identifier) @@ -205,15 +206,21 @@ class TheTVDb(ShowProvider): for episode in season.values(): if episode_identifier is not None and episode['id'] == toUnicode(episode_identifier): - result = self._parseEpisode(show, episode) + result = self._parseEpisode(episode) self.setCache(cache_key, result) return result else: - result.append(self._parseEpisode(show, episode)) + result.append(self._parseEpisode(episode)) self.setCache(cache_key, result) return result + def getIdentifier(self, value): + if type(value) is dict: + return value.get('thetvdb') + + return value + def _parseShow(self, show): # From c2c98f644bcf59c2132261c7a35f618817e50d90 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 22:56:26 +1200 Subject: [PATCH 16/59] [TV] Fixed matcher and provider events --- couchpotato/core/media/show/matcher.py | 4 ++-- couchpotato/core/media/show/providers/base.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/matcher.py mode change 100644 => 100755 couchpotato/core/media/show/providers/base.py diff --git a/couchpotato/core/media/show/matcher.py b/couchpotato/core/media/show/matcher.py old mode 100644 new mode 100755 index 7df36d1..006b48a --- a/couchpotato/core/media/show/matcher.py +++ b/couchpotato/core/media/show/matcher.py @@ -83,7 +83,7 @@ class Base(MatcherBase): class Episode(Base): - type = 'episode' + type = 'show.episode' def correctIdentifier(self, chain, media): identifier = self.getChainIdentifier(chain) @@ -108,7 +108,7 @@ class Episode(Base): return True class Season(Base): - type = 'season' + type = 'show.season' def correctIdentifier(self, chain, media): identifier = self.getChainIdentifier(chain) diff --git a/couchpotato/core/media/show/providers/base.py b/couchpotato/core/media/show/providers/base.py old mode 100644 new mode 100755 index 8ad4a7a..15ec388 --- a/couchpotato/core/media/show/providers/base.py +++ b/couchpotato/core/media/show/providers/base.py @@ -6,8 +6,8 @@ class ShowProvider(BaseInfoProvider): class SeasonProvider(BaseInfoProvider): - type = 'season' + type = 'show.season' class EpisodeProvider(BaseInfoProvider): - type = 'episode' + type = 'show.episode' From 44063dfcc5c6646574d8f46577fc13ba2ea6c31b Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 26 Jul 2014 23:00:41 +1200 Subject: [PATCH 17/59] [TV] Only expand/extend height when showing the episodes view --- couchpotato/core/media/show/_base/static/show.episodes.js | 2 +- couchpotato/core/media/show/_base/static/show.js | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js index ace93af..9c95051 100755 --- a/couchpotato/core/media/show/_base/static/show.episodes.js +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -27,7 +27,7 @@ var Episodes = new Class({ }); } - self.show.slide('in', self.container); + self.show.slide('in', self.container, true); }, createEpisodes: function() { diff --git a/couchpotato/core/media/show/_base/static/show.js b/couchpotato/core/media/show/_base/static/show.js index b91e637..42a7204 100755 --- a/couchpotato/core/media/show/_base/static/show.js +++ b/couchpotato/core/media/show/_base/static/show.js @@ -264,7 +264,7 @@ var Show = new Class({ return t; }, - slide: function(direction, el){ + slide: function(direction, el, expand){ var self = this; if(direction == 'in'){ @@ -278,8 +278,10 @@ var Show = new Class({ el.show(); - self.el.addClass('expanded'); - self.el.getElements('.table').addClass('expanded'); + if(expand === true) { + self.el.addClass('expanded'); + self.el.getElements('.table').addClass('expanded'); + } self.data_container.addClass('hide_right'); } From c77eaabbff18e2bdc601ba3382fae8b7100b72b7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 00:15:14 +1200 Subject: [PATCH 18/59] [TV] Update messages containing "movie", fixed alignment and search box --- couchpotato/core/media/show/_base/static/list.js | 16 +++--- couchpotato/core/media/show/_base/static/show.css | 68 +++++++++++------------ 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/list.js b/couchpotato/core/media/show/_base/static/list.js index a32e9b7..6a969ce 100755 --- a/couchpotato/core/media/show/_base/static/list.js +++ b/couchpotato/core/media/show/_base/static/list.js @@ -37,7 +37,7 @@ var ShowList = new Class({ 'html': self.options.description, 'styles': {'display': 'none'} }) : null, - self.movie_list = new Element('div'), + self.movie_list = new Element('div.list'), self.load_more = self.options.load_more ? new Element('a.load_more', { 'events': { 'click': self.loadMore.bind(self) @@ -79,7 +79,7 @@ var ShowList = new Class({ self.fireEvent('movieAdded', notification); if(self.options.add_new && !self.movies_added[notification.data._id] && notification.data.status == self.options.status){ window.scroll(0,0); - self.createMovie(notification.data, 'top'); + self.createShow(notification.data, 'top'); self.setCounter(self.counter_count+1); self.checkIfEmpty(); @@ -117,7 +117,7 @@ var ShowList = new Class({ } Object.each(movies, function(movie){ - self.createMovie(movie); + self.createShow(movie); }); self.total_movies += total; @@ -131,7 +131,7 @@ var ShowList = new Class({ if(!self.navigation_counter) return; self.counter_count = count; - self.navigation_counter.set('text', (count || 0) + ' movies'); + self.navigation_counter.set('text', (count || 0) + ' shows'); if (self.empty_message) { self.empty_message.destroy(); @@ -143,7 +143,7 @@ var ShowList = new Class({ (self.filter.starts_with ? ' in '+self.filter.starts_with+'' : ''); self.empty_message = new Element('.message', { - 'html': 'No movies found ' + message + '.
' + 'html': 'No shows found ' + message + '.
' }).grab( new Element('a', { 'text': 'Reset filter', @@ -167,20 +167,20 @@ var ShowList = new Class({ }, - createMovie: function(movie, inject_at){ + createShow: function(show, inject_at){ var self = this; var m = new Show(self, { 'actions': self.options.actions, 'view': self.current_view, 'onSelect': self.calculateSelected.bind(self) - }, movie); + }, show); $(m).inject(self.movie_list, inject_at || 'bottom'); m.fireEvent('injected'); self.movies.include(m); - self.movies_added[movie._id] = true; + self.movies_added[show._id] = true; }, createNavigation: function(){ diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index 14dc462..0f83a47 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -1,4 +1,4 @@ -.shows { +.shows.details_list { padding: 10px 0 20px; position: relative; z-index: 3; @@ -88,7 +88,7 @@ padding-top: 6px; } - .shows .show { + .shows .list .show { position: relative; margin: 10px 0; padding-left: 20px; @@ -100,11 +100,11 @@ background: rgba(0,0,0,.2); } - .shows .show.expanded { + .shows .list .show.expanded { height: 360px; } - .shows .show .table.expanded { + .shows .list .show .table.expanded { height: 360px; } @@ -113,7 +113,7 @@ background: none; } - .shows.details_list .show { + .shows.details_list .list .show { padding-left: 120px; } @@ -146,7 +146,7 @@ } } - .shows .show .mask { + .shows .list .show .mask { position: absolute; top: 0; left: 0; @@ -196,7 +196,7 @@ right: -100%; } - .shows .show .check { + .shows .list .show .check { display: none; } @@ -471,8 +471,8 @@ } } - .shows .show:hover .data .actions, - .touch_enabled .shows .show .data .actions { + .shows .list .show:hover .data .actions, + .touch_enabled .shows .list .show .data .actions { opacity: 1; display: inline-block; } @@ -482,7 +482,7 @@ bottom: 18px; } - .shows .show:hover .actions { + .shows .list .show:hover .actions { opacity: 1; display: inline-block; } @@ -492,8 +492,8 @@ top: auto; } - .shows .show:hover .action { opacity: 0.6; } - .shows .show:hover .action:hover { opacity: 1; } + .shows .list .show:hover .action { opacity: 0.6; } + .shows .list .show:hover .action:hover { opacity: 1; } .shows .data .action { display: inline-block; @@ -681,11 +681,11 @@ top: -30px; } - .shows .show .episodes .item { + .shows .list .show .episodes .item { position: relative; } - .shows .show .episodes .actions { + .shows .list .show .episodes .actions { position: absolute; width: auto; right: 0; @@ -693,36 +693,36 @@ border-left: none; } - .shows .show .episodes .actions .refresh { + .shows .list .show .episodes .actions .refresh { color: #cbeecc; } - .shows .show .try_container { + .shows .list .show .try_container { padding: 5px 10px; text-align: center; } - .shows .show .try_container a { + .shows .list .show .try_container a { margin: 0 5px; padding: 2px 5px; } - .shows .show .releases .next_release { + .shows .list .show .releases .next_release { border-left: 6px solid #2aa300; } - .shows .show .releases .next_release > :first-child { + .shows .list .show .releases .next_release > :first-child { margin-left: -6px; } - .shows .show .releases .last_release { + .shows .list .show .releases .last_release { border-left: 6px solid #ffa200; } - .shows .show .releases .last_release > :first-child { + .shows .list .show .releases .last_release > :first-child { margin-left: -6px; } - .shows .show .trynext { + .shows .list .show .trynext { display: inline; position: absolute; right: 180px; @@ -733,32 +733,32 @@ height: 100%; top: 0; } - .touch_enabled .shows .show .trynext { + .touch_enabled .shows .list .show .trynext { display: none; } @media all and (max-width: 480px) { - .shows .show .trynext { + .shows .list .show .trynext { display: none; } } .shows.mass_edit_list .trynext { display: none; } - .wanted .shows .show .trynext { + .wanted .shows .list .show .trynext { padding-right: 30px; } - .shows .show:hover .trynext, - .touch_enabled .shows.details_list .show .trynext { + .shows .list .show:hover .trynext, + .touch_enabled .shows.details_list .list .show .trynext { opacity: 1; } - .shows.details_list .show .trynext { + .shows.details_list .list .show .trynext { background: #47515f; padding: 0; right: 0; height: 25px; } - .shows .show .trynext a { + .shows .list .show .trynext a { background-position: 5px center; padding: 0 5px 0 25px; margin-right: 10px; @@ -767,19 +767,19 @@ line-height: 27px; font-family: OpenSans, "Helvetica Neue", Helvetica, Arial, Geneva, sans-serif; } - .shows .show .trynext a:before { + .shows .list .show .trynext a:before { margin: 2px 0 0 -20px; position: absolute; font-family: 'Elusive-Icons'; } - .shows.details_list .show .trynext a { + .shows.details_list .list .show .trynext a { line-height: 23px; } - .shows .show .trynext a:last-child { + .shows .list .show .trynext a:last-child { margin: 0; } - .shows .show .trynext a:hover, - .touch_enabled .shows .show .trynext a { + .shows .list .show .trynext a:hover, + .touch_enabled .shows .list .show .trynext a { background-color: #369545; } From bfdf565a0d79d2ebfbf06b8148796af4decc59b8 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 00:18:48 +1200 Subject: [PATCH 19/59] [TV] Changed show list to call "media.available_chars" correctly --- couchpotato/core/media/show/_base/static/list.js | 1 + 1 file changed, 1 insertion(+) diff --git a/couchpotato/core/media/show/_base/static/list.js b/couchpotato/core/media/show/_base/static/list.js index 6a969ce..87b085a 100755 --- a/couchpotato/core/media/show/_base/static/list.js +++ b/couchpotato/core/media/show/_base/static/list.js @@ -283,6 +283,7 @@ var ShowList = new Class({ if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) Api.request('media.available_chars', { 'data': Object.merge({ + 'type': 'show', 'status': self.options.status }, self.filter), 'onSuccess': function(json){ From a1ca367037077dfac854e1ceb407613326ab6933 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 01:03:58 +1200 Subject: [PATCH 20/59] Include releases in "library.tree" --- couchpotato/core/media/_base/library/main.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index b565d16..2f5629d 100755 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -82,10 +82,12 @@ class Library(LibraryBase): result = media db = get_db() - items = db.get_many('media_children', media['_id'], with_doc = True) + # Find children + items = db.get_many('media_children', media['_id'], with_doc = True) keys = [] + # Build children arrays for item in items: parts = item['doc']['type'].split('.') key = parts[-1] + 's' @@ -98,7 +100,11 @@ class Library(LibraryBase): result[key][item['_id']] = fireEvent('library.tree', item['doc'], single = True) + # Unique children for key in keys: result[key] = result[key].values() + # Include releases + result['releases'] = fireEvent('release.for_media', media['_id'], single = True) + return result From 8fe3d6f58f5bdd46f89a71dd56224b9464ccbc6e Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 01:05:41 +1200 Subject: [PATCH 21/59] [TV] Adjust episode table column size, added quality indicators --- .../core/media/show/_base/static/episode.js | 58 ++++++++++++++++++++-- couchpotato/core/media/show/_base/static/show.css | 28 ++++++++--- 2 files changed, 77 insertions(+), 9 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/episode.js b/couchpotato/core/media/show/_base/static/episode.js index 2ba18a9..37d2f16 100755 --- a/couchpotato/core/media/show/_base/static/episode.js +++ b/couchpotato/core/media/show/_base/static/episode.js @@ -10,8 +10,10 @@ var Episode = new Class({ self.show = show; self.data = data; - self.el = new Element('div.item'); - self.el_actions = new Element('div.actions'); + self.profile = self.show.profile; + + self.el = new Element('div.item.data'); + self.el_actions = new Element('div.episode-actions'); self.create(); }, @@ -24,11 +26,14 @@ var Episode = new Class({ self.el.adopt( new Element('span.episode', {'text': (self.data.info.number || 0)}), new Element('span.name', {'text': self.getTitle()}), - new Element('span.firstaired', {'text': self.data.info.firstaired}) + new Element('span.firstaired', {'text': self.data.info.firstaired}), + + self.quality = new Element('span.quality') ); self.el_actions.inject(self.el); + // imdb if(self.data.identifiers && self.data.identifiers.imdb) { new Element('a.imdb.icon2', { 'title': 'Go to the IMDB page of ' + self.show.getTitle(), @@ -37,12 +42,59 @@ var Episode = new Class({ }).inject(self.el_actions); } + // refresh new Element('a.refresh.icon2', { 'title': 'Refresh the episode info and do a forced search', 'events': { 'click': self.doRefresh.bind(self) } }).inject(self.el_actions); + + // Add profile + if(self.profile.data) { + self.profile.getTypes().each(function(type){ + var q = self.addQuality(type.get('quality'), type.get('3d')); + + if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ + q.addClass('finish'); + q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') + } + }); + } + + // Add releases + self.updateReleases(); + }, + + updateReleases: function(){ + var self = this; + if(!self.data.releases || self.data.releases.length == 0) return; + + self.data.releases.each(function(release){ + + var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')), + status = release.status; + + if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) + q = self.addQuality(release.quality, release.is_3d || false); + + if (q && !q.hasClass(status)){ + q.addClass(status); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status) + } + + }); + }, + + addQuality: function(quality, is_3d){ + var self = this, + q = Quality.getQuality(quality); + + return new Element('span', { + 'text': q.label + (is_3d ? ' 3D' : ''), + 'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''), + 'title': '' + }).inject(self.quality); }, getTitle: function(){ diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index 0f83a47..5c4c9fb 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -680,22 +680,38 @@ .hide_trailer.hide { top: -30px; } - - .shows .list .show .episodes .item { + .shows .list .episodes .item { position: relative; + width: auto; + height: auto; + padding: 0; } - .shows .list .show .episodes .actions { + .shows .list .episodes .item span.episode { + width: 40px; + padding: 0 10px; + } + + .shows .list .episodes .item span.name { + width: 280px; + } + + .shows .list .episodes .item span.firstaired { + width: 80px; + } + + .shows .list .show .episodes .episode-actions { position: absolute; width: auto; right: 0; + top: 0; border-left: none; } - .shows .list .show .episodes .actions .refresh { - color: #cbeecc; - } + .shows .list .show .episodes .episode-actions .refresh { + color: #cbeecc; + } .shows .list .show .try_container { padding: 5px 10px; From d79556f36f66506b6ae034b409fd2bb6e440ca65 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 03:57:12 +1200 Subject: [PATCH 22/59] [TV] Moved imdb and refresh components to new "episode.actions.js", implemented episode "release" action --- .../media/show/_base/static/episode.actions.js | 450 +++++++++++++++++++++ .../core/media/show/_base/static/episode.js | 58 ++- couchpotato/core/media/show/_base/static/show.css | 32 +- .../core/media/show/_base/static/show.episodes.js | 5 +- couchpotato/core/media/show/_base/static/show.js | 4 +- 5 files changed, 510 insertions(+), 39 deletions(-) create mode 100755 couchpotato/core/media/show/_base/static/episode.actions.js diff --git a/couchpotato/core/media/show/_base/static/episode.actions.js b/couchpotato/core/media/show/_base/static/episode.actions.js new file mode 100755 index 0000000..b0f7a84 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/episode.actions.js @@ -0,0 +1,450 @@ +var EpisodeAction = new Class({ + + Implements: [Options], + + class_name: 'action icon2', + + initialize: function(episode, options){ + var self = this; + self.setOptions(options); + + self.show = episode.show; + self.episode = episode; + + self.create(); + if(self.el) + self.el.addClass(self.class_name) + }, + + create: function(){}, + + disable: function(){ + if(this.el) + this.el.addClass('disable') + }, + + enable: function(){ + if(this.el) + this.el.removeClass('disable') + }, + + getTitle: function(){ + var self = this; + + try { + return self.show.getTitle(); + } + catch(e){ + try { + return self.show.original_title ? self.show.original_title : self.show.titles[0]; + } + catch(e){ + return 'Unknown'; + } + } + }, + + get: function(key){ + var self = this; + try { + return self.show.get(key) + } + catch(e){ + return self.show[key] + } + }, + + createMask: function(){ + var self = this; + self.mask = new Element('div.mask', { + 'styles': { + 'z-index': '1' + } + }).inject(self.show, 'top').fade('hide'); + }, + + toElement: function(){ + return this.el || null + } + +}); + +var EA = {}; + +EA.IMDB = new Class({ + + Extends: EpisodeAction, + id: null, + + create: function(){ + var self = this; + + self.id = self.show.getIdentifier ? self.show.getIdentifier() : self.get('imdb'); + + self.el = new Element('a.imdb', { + 'title': 'Go to the IMDB page of ' + self.getTitle(), + 'href': 'http://www.imdb.com/title/'+self.id+'/', + 'target': '_blank' + }); + + if(!self.id) self.disable(); + } + +}); + +EA.Release = new Class({ + + Extends: EpisodeAction, + + create: function(){ + var self = this; + + self.el = new Element('a.releases.download', { + 'title': 'Show the releases that are available for ' + self.getTitle(), + 'events': { + 'click': self.open.bind(self) + } + }); + + if(!self.episode.data.releases || self.episode.data.releases.length == 0) + self.el.hide(); + else + self.showHelper(); + + App.on('show.searcher.ended', function(notification){ + if(self.show.data._id != notification.data._id) return; + + self.releases = null; + if(self.options_container){ + self.options_container.destroy(); + self.options_container = null; + } + }); + + }, + + open: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + self.createReleases(); + + }, + + createReleases: function(){ + var self = this; + + if(!self.releases_container){ + self.releases_container = new Element('div.releases.table').inject(self.episode.el); + + // Header + new Element('div.item.head').adopt( + new Element('span.name', {'text': 'Release name'}), + new Element('span.status', {'text': 'Status'}), + new Element('span.quality', {'text': 'Quality'}), + new Element('span.size', {'text': 'Size'}), + new Element('span.age', {'text': 'Age'}), + new Element('span.score', {'text': 'Score'}), + new Element('span.provider', {'text': 'Provider'}) + ).inject(self.releases_container); + + if(self.episode.data.releases) + self.episode.data.releases.each(function(release){ + + var quality = Quality.getQuality(release.quality) || {}, + info = release.info || {}, + provider = self.get(release, 'provider') + (info['provider_extra'] ? self.get(release, 'provider_extra') : ''); + + var release_name = self.get(release, 'name'); + if(release.files && release.files.length > 0){ + try { + var movie_file = release.files.filter(function(file){ + var type = File.Type.get(file.type_id); + return type && type.identifier == 'movie' + }).pick(); + release_name = movie_file.path.split(Api.getOption('path_sep')).getLast(); + } + catch(e){} + } + + // Create release + release['el'] = new Element('div', { + 'class': 'item '+release.status, + 'id': 'release_'+release._id + }).adopt( + new Element('span.name', {'text': release_name, 'title': release_name}), + new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), + new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}), + new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), + new Element('span.age', {'text': self.get(release, 'age')}), + new Element('span.score', {'text': self.get(release, 'score')}), + new Element('span.provider', { 'text': provider, 'title': provider }), + info['detail_url'] ? new Element('a.info.icon2', { + 'href': info['detail_url'], + 'target': '_blank' + }) : new Element('a'), + new Element('a.download.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + if(!this.hasClass('completed')) + self.download(release); + } + } + }), + new Element('a.delete.icon2', { + 'events': { + 'click': function(e){ + (e).preventDefault(); + self.ignore(release); + } + } + }) + ).inject(self.releases_container); + + if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ + if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) + self.last_release = release; + } + else if(!self.next_release && release.status == 'available'){ + self.next_release = release; + } + + var update_handle = function(notification) { + if(notification.data._id != release._id) return; + + var q = self.show.quality.getElement('.q_' + release.quality), + new_status = notification.data.status; + + release.el.set('class', 'item ' + new_status); + + var status_el = release.el.getElement('.release_status'); + status_el.set('class', 'release_status ' + new_status); + status_el.set('text', new_status); + + if(!q && (new_status == 'snatched' || new_status == 'seeding' || new_status == 'done')) + q = self.addQuality(release.quality_id); + + if(q && !q.hasClass(new_status)) { + q.removeClass(release.status).addClass(new_status); + q.set('title', q.get('title').replace(release.status, new_status)); + } + }; + + App.on('release.update_status', update_handle); + + }); + + if(self.last_release) + self.releases_container.getElements('#release_'+self.last_release._id).addClass('last_release'); + + if(self.next_release) + self.releases_container.getElements('#release_'+self.next_release._id).addClass('next_release'); + + if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){ + + self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_container, 'top'); + + var nr = self.next_release, + lr = self.last_release; + + self.trynext_container.adopt( + new Element('span.or', { + 'text': 'If anything went wrong, download' + }), + lr ? new Element('a.button.orange', { + 'text': 'the same release again', + 'events': { + 'click': function(){ + self.download(lr); + } + } + }) : null, + nr && lr ? new Element('span.or', { + 'text': ',' + }) : null, + nr ? [new Element('a.button.green', { + 'text': lr ? 'another release' : 'the best release', + 'events': { + 'click': function(){ + self.download(nr); + } + } + }), + new Element('span.or', { + 'text': 'or pick one below' + })] : null + ) + } + + self.last_release = null; + self.next_release = null; + + self.episode.el.addEvent('outerClick', function(){ + console.log('outerClick'); + + self.releases_container.removeClass('expanded'); + }); + } + + self.releases_container.addClass('expanded'); + + }, + + showHelper: function(e){ + var self = this; + if(e) + (e).preventDefault(); + + var has_available = false, + has_snatched = false; + + if(self.episode.data.releases) + self.episode.data.releases.each(function(release){ + if(has_available && has_snatched) return; + + if(['snatched', 'downloaded', 'seeding'].contains(release.status)) + has_snatched = true; + + if(['available'].contains(release.status)) + has_available = true; + + }); + + if(has_available || has_snatched){ + + self.trynext_container = new Element('div.buttons.trynext').inject(self.show.info_container); + + self.trynext_container.adopt( + has_available ? [new Element('a.icon2.readd', { + 'text': has_snatched ? 'Download another release' : 'Download the best release', + 'events': { + 'click': self.tryNextRelease.bind(self) + } + }), + new Element('a.icon2.download', { + 'text': 'pick one yourself', + 'events': { + 'click': function(){ + self.show.quality.fireEvent('click'); + } + } + })] : null, + new Element('a.icon2.completed', { + 'text': 'mark this movie done', + 'events': { + 'click': self.markMovieDone.bind(self) + } + }) + ) + } + + }, + + get: function(release, type){ + return (release.info && release.info[type] !== undefined) ? release.info[type] : 'n/a' + }, + + download: function(release){ + var self = this; + + var release_el = self.releases_container.getElement('#release_'+release._id), + icon = release_el.getElement('.download.icon2'); + + if(icon) + icon.addClass('icon spinner').removeClass('download'); + + Api.request('release.manual_download', { + 'data': { + 'id': release._id + }, + 'onComplete': function(json){ + if(icon) + icon.removeClass('icon spinner'); + + if(json.success){ + if(icon) + icon.addClass('completed'); + release_el.getElement('.release_status').set('text', 'snatched'); + } + else + if(icon) + icon.addClass('attention').set('title', 'Something went wrong when downloading, please check logs.'); + } + }); + }, + + ignore: function(release){ + + Api.request('release.ignore', { + 'data': { + 'id': release._id + } + }) + + }, + + markMovieDone: function(){ + var self = this; + + Api.request('media.delete', { + 'data': { + 'id': self.show.get('_id'), + 'delete_from': 'wanted' + }, + 'onComplete': function(){ + var movie = $(self.show); + movie.set('tween', { + 'duration': 300, + 'onComplete': function(){ + self.show.destroy() + } + }); + movie.tween('height', 0); + } + }); + + }, + + tryNextRelease: function(){ + var self = this; + + Api.request('movie.searcher.try_next', { + 'data': { + 'media_id': self.show.get('_id') + } + }); + + } + +}); + +EA.Refresh = new Class({ + + Extends: EpisodeAction, + + create: function(){ + var self = this; + + self.el = new Element('a.refresh', { + 'title': 'Refresh the movie info and do a forced search', + 'events': { + 'click': self.doRefresh.bind(self) + } + }); + + }, + + doRefresh: function(e){ + var self = this; + (e).preventDefault(); + + Api.request('media.refresh', { + 'data': { + 'id': self.show.get('_id') + } + }); + } + +}); \ No newline at end of file diff --git a/couchpotato/core/media/show/_base/static/episode.js b/couchpotato/core/media/show/_base/static/episode.js index 37d2f16..821e946 100755 --- a/couchpotato/core/media/show/_base/static/episode.js +++ b/couchpotato/core/media/show/_base/static/episode.js @@ -4,16 +4,19 @@ var Episode = new Class({ action: {}, - initialize: function(show, data){ + initialize: function(show, options, data){ var self = this; + self.setOptions(options); self.show = show; + self.options = options; self.data = data; self.profile = self.show.profile; - self.el = new Element('div.item.data'); - self.el_actions = new Element('div.episode-actions'); + self.el = new Element('div.item').adopt( + self.detail = new Element('div.item.data') + ); self.create(); }, @@ -21,35 +24,17 @@ var Episode = new Class({ create: function(){ var self = this; - self.el.set('id', 'episode_'+self.data._id); + self.detail.set('id', 'episode_'+self.data._id); - self.el.adopt( + self.detail.adopt( new Element('span.episode', {'text': (self.data.info.number || 0)}), new Element('span.name', {'text': self.getTitle()}), new Element('span.firstaired', {'text': self.data.info.firstaired}), - self.quality = new Element('span.quality') + self.quality = new Element('span.quality'), + self.actions = new Element('div.episode-actions') ); - self.el_actions.inject(self.el); - - // imdb - if(self.data.identifiers && self.data.identifiers.imdb) { - new Element('a.imdb.icon2', { - 'title': 'Go to the IMDB page of ' + self.show.getTitle(), - 'href': 'http://www.imdb.com/title/' + self.data.identifiers.imdb + '/', - 'target': '_blank' - }).inject(self.el_actions); - } - - // refresh - new Element('a.refresh.icon2', { - 'title': 'Refresh the episode info and do a forced search', - 'events': { - 'click': self.doRefresh.bind(self) - } - }).inject(self.el_actions); - // Add profile if(self.profile.data) { self.profile.getTypes().each(function(type){ @@ -64,6 +49,12 @@ var Episode = new Class({ // Add releases self.updateReleases(); + + Object.each(self.options.actions, function(action, key){ + self.action[key.toLowerCase()] = action = new self.options.actions[key](self); + if(action.el) + self.actions.adopt(action) + }); }, updateReleases: function(){ @@ -111,13 +102,18 @@ var Episode = new Class({ return title; }, - doRefresh: function(e) { + getIdentifier: function(){ var self = this; - Api.request('media.refresh', { - 'data': { - 'id': self.data._id - } - }); + try { + return self.get('identifiers').imdb; + } + catch (e){ } + + return self.get('imdb'); + }, + + get: function(attr){ + return this.data[attr] || this.data.info[attr] } }); \ No newline at end of file diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index 5c4c9fb..c084d8e 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -682,25 +682,47 @@ } .shows .list .episodes .item { position: relative; - width: auto; + width: 100%; height: auto; padding: 0; + + text-align: left; + + transition: all .6s cubic-bezier(0.9,0,0.1,1); } - .shows .list .episodes .item span.episode { + .shows .list .episodes .item.data span.episode { width: 40px; padding: 0 10px; } - .shows .list .episodes .item span.name { + .shows .list .episodes .item.data span.name { width: 280px; } - .shows .list .episodes .item span.firstaired { + .shows .list .episodes .item.data span.firstaired { width: 80px; } - .shows .list .show .episodes .episode-actions { + .shows .list .episodes .item.data span.quality { + bottom: auto; + } + + .shows .list .episodes .releases.table { + display: none; + width: 100%; + height: auto; + + padding: 0; + + background: rgba(0,0,0,.2); + } + + .shows .list .episodes .releases.table.expanded { + display: block; + } + + .shows .list .episodes .episode-actions { position: absolute; width: auto; right: 0; diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js index 9c95051..5fa6645 100755 --- a/couchpotato/core/media/show/_base/static/show.episodes.js +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -1,8 +1,9 @@ var Episodes = new Class({ - initialize: function(show) { + initialize: function(show, options) { var self = this; self.show = show; + self.options = options; }, open: function(){ @@ -65,7 +66,7 @@ var Episodes = new Class({ createEpisode: function(episode){ var self = this, - e = new Episode(self.show, episode); + e = new Episode(self.show, self.options, episode); $(e).inject(self.episodes_container); }, diff --git a/couchpotato/core/media/show/_base/static/show.js b/couchpotato/core/media/show/_base/static/show.js index 42a7204..99b4297 100755 --- a/couchpotato/core/media/show/_base/static/show.js +++ b/couchpotato/core/media/show/_base/static/show.js @@ -13,7 +13,9 @@ var Show = new Class({ self.el = new Element('div.show'); - self.episodes = new Episodes(self); + self.episodes = new Episodes(self, { + 'actions': [EA.IMDB, EA.Release, EA.Refresh] + }); self.profile = Quality.getProfile(data.profile_id) || {}; self.category = CategoryList.getCategory(data.category_id) || {}; From 43275297e9ef06b3a9f2b9ca3d717e63c5247766 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 15:05:51 +1200 Subject: [PATCH 23/59] [TV] Improved episode actions drop-down (releases) --- .../media/show/_base/static/episode.actions.js | 54 ++++++++++++++++------ .../core/media/show/_base/static/episode.js | 11 ++++- couchpotato/core/media/show/_base/static/show.css | 27 +++++++++-- 3 files changed, 71 insertions(+), 21 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/episode.actions.js b/couchpotato/core/media/show/_base/static/episode.actions.js index b0f7a84..fc2a0a2 100755 --- a/couchpotato/core/media/show/_base/static/episode.actions.js +++ b/couchpotato/core/media/show/_base/static/episode.actions.js @@ -102,10 +102,12 @@ EA.Release = new Class({ self.el = new Element('a.releases.download', { 'title': 'Show the releases that are available for ' + self.getTitle(), 'events': { - 'click': self.open.bind(self) + 'click': self.toggle.bind(self) } }); + self.options = new Element('div.episode-options').inject(self.episode.el); + if(!self.episode.data.releases || self.episode.data.releases.length == 0) self.el.hide(); else @@ -123,8 +125,19 @@ EA.Release = new Class({ }, + toggle: function(e){ + var self = this; + + if(self.options && self.options.hasClass('expanded')) { + self.close(); + } else { + self.open(); + } + }, + open: function(e){ var self = this; + if(e) (e).preventDefault(); @@ -132,11 +145,23 @@ EA.Release = new Class({ }, + close: function(e) { + var self = this; + + if(e) + (e).preventDefault(); + + self.options.setStyle('height', 0) + .removeClass('expanded'); + }, + createReleases: function(){ var self = this; - if(!self.releases_container){ - self.releases_container = new Element('div.releases.table').inject(self.episode.el); + if(!self.releases_table){ + self.options.adopt( + self.releases_table = new Element('div.releases.table') + ); // Header new Element('div.item.head').adopt( @@ -147,7 +172,7 @@ EA.Release = new Class({ new Element('span.age', {'text': 'Age'}), new Element('span.score', {'text': 'Score'}), new Element('span.provider', {'text': 'Provider'}) - ).inject(self.releases_container); + ).inject(self.releases_table); if(self.episode.data.releases) self.episode.data.releases.each(function(release){ @@ -174,7 +199,7 @@ EA.Release = new Class({ 'id': 'release_'+release._id }).adopt( new Element('span.name', {'text': release_name, 'title': release_name}), - new Element('span.status', {'text': release.status, 'class': 'release_status '+release.status}), + new Element('span.status', {'text': release.status, 'class': 'status '+release.status}), new Element('span.quality', {'text': quality.label + (release.is_3d ? ' 3D' : '') || 'n/a'}), new Element('span.size', {'text': info['size'] ? Math.floor(self.get(release, 'size')) : 'n/a'}), new Element('span.age', {'text': self.get(release, 'age')}), @@ -201,7 +226,7 @@ EA.Release = new Class({ } } }) - ).inject(self.releases_container); + ).inject(self.releases_table); if(release.status == 'ignored' || release.status == 'failed' || release.status == 'snatched'){ if(!self.last_release || (self.last_release && self.last_release.status != 'snatched' && release.status == 'snatched')) @@ -237,14 +262,14 @@ EA.Release = new Class({ }); if(self.last_release) - self.releases_container.getElements('#release_'+self.last_release._id).addClass('last_release'); + self.releases_table.getElements('#release_'+self.last_release._id).addClass('last_release'); if(self.next_release) - self.releases_container.getElements('#release_'+self.next_release._id).addClass('next_release'); + self.releases_table.getElements('#release_'+self.next_release._id).addClass('next_release'); if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status) === false)){ - self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_container, 'top'); + self.trynext_container = new Element('div.buttons.try_container').inject(self.releases_table, 'top'); var nr = self.next_release, lr = self.last_release; @@ -282,13 +307,12 @@ EA.Release = new Class({ self.next_release = null; self.episode.el.addEvent('outerClick', function(){ - console.log('outerClick'); - - self.releases_container.removeClass('expanded'); + self.close(); }); } - self.releases_container.addClass('expanded'); + self.options.setStyle('height', self.releases_table.getSize().y) + .addClass('expanded'); }, @@ -349,7 +373,7 @@ EA.Release = new Class({ download: function(release){ var self = this; - var release_el = self.releases_container.getElement('#release_'+release._id), + var release_el = self.releases_table.getElement('#release_'+release._id), icon = release_el.getElement('.download.icon2'); if(icon) @@ -442,7 +466,7 @@ EA.Refresh = new Class({ Api.request('media.refresh', { 'data': { - 'id': self.show.get('_id') + 'id': self.episode.get('_id') } }); } diff --git a/couchpotato/core/media/show/_base/static/episode.js b/couchpotato/core/media/show/_base/static/episode.js index 821e946..b7e73f3 100755 --- a/couchpotato/core/media/show/_base/static/episode.js +++ b/couchpotato/core/media/show/_base/static/episode.js @@ -31,7 +31,16 @@ var Episode = new Class({ new Element('span.name', {'text': self.getTitle()}), new Element('span.firstaired', {'text': self.data.info.firstaired}), - self.quality = new Element('span.quality'), + self.quality = new Element('span.quality', { + 'events': { + 'click': function(e){ + var releases = self.detail.getElement('.episode-actions .releases'); + + if(releases.isVisible()) + releases.fireEvent('click', [e]) + } + } + }), self.actions = new Element('div.episode-actions') ); diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index c084d8e..f8d7bf0 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -708,19 +708,36 @@ bottom: auto; } + .shows .list .episodes .episode-options { + display: block; + + width: 100%; + height: 0; + min-height: 0; + + padding: 0; + + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); + transition-property: width, height; + + overflow: hidden; + } + .shows .list .episodes .releases.table { - display: none; width: 100%; height: auto; - padding: 0; background: rgba(0,0,0,.2); } - .shows .list .episodes .releases.table.expanded { - display: block; - } + .shows .list .episodes .releases.table span.name { + width: 300px; + } + + .shows .list .episodes .releases.table span.status { + width: 85px; + } .shows .list .episodes .episode-actions { position: absolute; From f3ae8a05ccf6d69e1cf0f28e04fa08395c5c16b7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 15:07:52 +1200 Subject: [PATCH 24/59] [TV] Added "status" to episode and season media --- couchpotato/core/media/show/episode.py | 3 ++- couchpotato/core/media/show/season.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index e184e90..754a48f 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -16,7 +16,7 @@ class Episode(MediaBase): addEvent('show.episode.add', self.add) addEvent('show.episode.update_info', self.updateInfo) - def add(self, parent_id, info = None, update_after = True): + def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} identifiers = info.get('identifiers') @@ -28,6 +28,7 @@ class Episode(MediaBase): '_t': 'media', 'type': 'show.episode', 'identifiers': identifiers, + 'status': status if status else 'active', 'parent_id': parent_id, 'info': info, # Returned dict by providers } diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index 6197c9c..6c9ff09 100755 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -16,7 +16,7 @@ class Season(MediaBase): addEvent('show.season.add', self.add) addEvent('show.season.update_info', self.updateInfo) - def add(self, parent_id, info = None, update_after = True): + def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} identifiers = info.get('identifiers') @@ -30,6 +30,7 @@ class Season(MediaBase): '_t': 'media', 'type': 'show.season', 'identifiers': identifiers, + 'status': status if status else 'active', 'parent_id': parent_id, 'info': info, # Returned dict by providers } From bda44848a11f4f70b314360d9c8f7fae37003201 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 15:08:28 +1200 Subject: [PATCH 25/59] [TV] Added "full_search" placeholder methods to avoid errors on startup --- couchpotato/core/media/show/searcher/episode.py | 17 ++++++++++++++++- couchpotato/core/media/show/searcher/season.py | 18 +++++++++++++++++- couchpotato/core/media/show/searcher/show.py | 19 +++++++++++++++++-- 3 files changed, 50 insertions(+), 4 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/searcher/season.py mode change 100644 => 100755 couchpotato/core/media/show/searcher/show.py diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py index ace9768..7e92537 100755 --- a/couchpotato/core/media/show/searcher/episode.py +++ b/couchpotato/core/media/show/searcher/episode.py @@ -1,6 +1,6 @@ from couchpotato import fireEvent, get_db, Env from couchpotato.api import addApiView -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEventAsync from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.main import SearchSetupError @@ -19,11 +19,26 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): def __init__(self): super(EpisodeSearcher, self).__init__() + addEvent('%s.searcher.all' % self.getType(), self.searchAll) addEvent('%s.searcher.single' % self.getType(), self.single) addEvent('searcher.correct_release', self.correctRelease) + addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = { + 'desc': 'Starts a full search for all wanted shows', + }) + addApiView('%s.searcher.single' % self.getType(), self.singleView) + def searchAllView(self, **kwargs): + fireEventAsync('%s.searcher.all' % self.getType(), manual = True) + + return { + 'success': not self.in_progress + } + + def searchAll(self, manual = False): + pass + def singleView(self, media_id, **kwargs): db = get_db() media = db.get('id', media_id) diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py old mode 100644 new mode 100755 index 64fd263..b1fe630 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -1,4 +1,5 @@ -from couchpotato.core.event import addEvent +from couchpotato.api import addApiView +from couchpotato.core.event import addEvent, fireEventAsync from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media.show import ShowTypeBase @@ -16,8 +17,23 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): def __init__(self): super(SeasonSearcher, self).__init__() + addEvent('%s.searcher.all' % self.getType(), self.searchAll) addEvent('%s.searcher.single' % self.getType(), self.single) + addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = { + 'desc': 'Starts a full search for all wanted seasons', + }) + + def searchAllView(self, **kwargs): + fireEventAsync('%s.searcher.all' % self.getType(), manual = True) + + return { + 'success': not self.in_progress + } + + def searchAll(self, manual = False): + pass + def single(self, media, show, profile): # Check if any episode is already snatched diff --git a/couchpotato/core/media/show/searcher/show.py b/couchpotato/core/media/show/searcher/show.py old mode 100644 new mode 100755 index 49fb775..07d644b --- a/couchpotato/core/media/show/searcher/show.py +++ b/couchpotato/core/media/show/searcher/show.py @@ -1,5 +1,6 @@ from couchpotato import get_db -from couchpotato.core.event import fireEvent, addEvent +from couchpotato.api import addApiView +from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.helpers.variable import getTitle from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase @@ -19,10 +20,24 @@ class ShowSearcher(SearcherBase, ShowTypeBase): def __init__(self): super(ShowSearcher, self).__init__() + addEvent('%s.searcher.all' % self.getType(), self.searchAll) addEvent('%s.searcher.single' % self.getType(), self.single) - addEvent('searcher.get_search_title', self.getSearchTitle) + addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = { + 'desc': 'Starts a full search for all wanted episodes', + }) + + def searchAllView(self, **kwargs): + fireEventAsync('%s.searcher.all' % self.getType(), manual = True) + + return { + 'success': not self.in_progress + } + + def searchAll(self, manual = False): + pass + def single(self, media, search_protocols = None, manual = False): # Find out search type try: From 479e20d8f3f3b4aed9be9173377c144ddf9f3828 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 27 Jul 2014 23:51:44 +1200 Subject: [PATCH 26/59] [TV] Added "eta" display placeholder (data not there yet) --- couchpotato/core/media/show/_base/static/show.css | 26 +++++++++++++++++++++++ couchpotato/core/media/show/_base/static/show.js | 21 +++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index f8d7bf0..0b223b7 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -378,6 +378,32 @@ display: none; } + .shows .data .eta { + display: none; + } + + .shows.details_list .data .eta { + position: absolute; + bottom: 0; + right: 0; + display: block; + min-height: 20px; + text-align: right; + font-style: italic; + opacity: .8; + font-size: 11px; + } + + .shows.details_list .movie:hover .data .eta { + display: none; + } + + .shows.thumbs_list .data .eta { + display: block; + position: absolute; + bottom: 40px; + } + .shows .data .quality { position: absolute; bottom: 2px; diff --git a/couchpotato/core/media/show/_base/static/show.js b/couchpotato/core/media/show/_base/static/show.js index 99b4297..6019721 100755 --- a/couchpotato/core/media/show/_base/static/show.js +++ b/couchpotato/core/media/show/_base/static/show.js @@ -140,7 +140,22 @@ var Show = new Class({ self.el.addClass('status_'+self.get('status')); - self.el.adopt( + var eta = null, + eta_date = null, + now = Math.round(+new Date()/1000); + + if(self.data.info.release_date) + [self.data.info.release_date.dvd, self.data.info.release_date.theater].each(function(timestamp){ + if (timestamp > 0 && (eta == null || Math.abs(timestamp - now) < Math.abs(eta - now))) + eta = timestamp; + }); + + if(eta){ + eta_date = new Date(eta * 1000); + eta_date = eta_date.toLocaleString('en-us', { month: "long" }) + ' ' + eta_date.getFullYear(); + } + + self.el.adopt( self.select_checkbox = new Element('input[type=checkbox].inlay', { 'events': { 'change': function(){ @@ -170,6 +185,10 @@ var Show = new Class({ self.description = new Element('div.description.tiny_scroll', { 'text': self.data.info.plot }), + self.eta = eta_date && (now+8035200 > eta) ? new Element('div.eta', { + 'text': eta_date, + 'title': 'ETA' + }) : null, self.quality = new Element('div.quality', { 'events': { 'click': function(e){ From 4de9879927c115cb64a303f7435831c4f2308332 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 00:42:10 +1200 Subject: [PATCH 27/59] [TV] Fixed dashboard issues with shows --- couchpotato/core/plugins/dashboard.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) mode change 100644 => 100755 couchpotato/core/plugins/dashboard.py diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py old mode 100644 new mode 100755 index 776f24e..1f5fdcd --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -69,16 +69,16 @@ class Dashboard(Plugin): coming_soon = False # Theater quality - if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info']['year'], single = True): + if pp.get('theater') and fireEvent('movie.searcher.could_be_released', True, eta, media['info'].get('year'), single = True): coming_soon = True - elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info']['year'], single = True): + elif pp.get('dvd') and fireEvent('movie.searcher.could_be_released', False, eta, media['info'].get('year'), single = True): coming_soon = True if coming_soon: # Don't list older movies - if ((not late and (media['info']['year'] >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or - (late and (media['info']['year'] < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))): + if ((not late and (media['info'].get('year') >= now_year - 1) and (not eta.get('dvd') and not eta.get('theater') or eta.get('dvd') and eta.get('dvd') > (now - 2419200))) or + (late and (media['info'].get('year') < now_year - 1 or (eta.get('dvd', 0) > 0 or eta.get('theater')) and eta.get('dvd') < (now - 2419200)))): add = True From 3e05bc8d7828b496894d1457e72eb44045fdbec7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:06:05 +1200 Subject: [PATCH 28/59] Added "find" helper function --- couchpotato/core/helpers/variable.py | 8 ++++++++ 1 file changed, 8 insertions(+) mode change 100644 => 100755 couchpotato/core/helpers/variable.py diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py old mode 100644 new mode 100755 index fc844aa..db68da2 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -380,3 +380,11 @@ def getFreeSpace(directories): free_space[folder] = size return free_space + + +def find(func, iterable): + for item in iterable: + if func(item): + return item + + return None From aa92d76eb4e3959660402ef446737e6f9ef20462 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:06:51 +1200 Subject: [PATCH 29/59] Added "media_id" parameter to "library.tree" event --- couchpotato/core/media/_base/library/main.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index 2f5629d..da526ec 100755 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -78,13 +78,18 @@ class Library(LibraryBase): return cur - def tree(self, media): - result = media - + def tree(self, media = None, media_id = None): db = get_db() + if media: + result = media + elif media_id: + result = db.get('id', media_id, with_doc = True) + else: + return None + # Find children - items = db.get_many('media_children', media['_id'], with_doc = True) + items = db.get_many('media_children', result['_id'], with_doc = True) keys = [] # Build children arrays @@ -94,6 +99,8 @@ class Library(LibraryBase): if key not in result: result[key] = {} + elif type(result[key]) is not dict: + result[key] = {} if key not in keys: keys.append(key) @@ -105,6 +112,6 @@ class Library(LibraryBase): result[key] = result[key].values() # Include releases - result['releases'] = fireEvent('release.for_media', media['_id'], single = True) + result['releases'] = fireEvent('release.for_media', result['_id'], single = True) return result From 88f8cd708b469623d3a0c0f0214036c5b26c0d1f Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:11:20 +1200 Subject: [PATCH 30/59] [TV] Implemented fast show updates, working on "update_info" restructure --- couchpotato/core/media/show/_base/main.py | 69 ++++++++++++++++++++++--------- couchpotato/core/media/show/episode.py | 29 +++++++------ couchpotato/core/media/show/season.py | 33 +++++++++------ 3 files changed, 85 insertions(+), 46 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index da70c3d..7da66ae 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -5,7 +5,7 @@ from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.helpers.encoding import simplifyString -from couchpotato.core.helpers.variable import getTitle +from couchpotato.core.helpers.variable import getTitle, find from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase from qcond import QueryCondenser @@ -79,15 +79,13 @@ class ShowBase(MediaBase): '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) seasons_info = info.get('seasons', {}) identifiers = info.get('identifiers', {}) # Make sure we don't nest in_wanted data - del info['identifiers'] + try: del info['identifiers'] + except: pass try: del info['in_wanted'] except: pass try: del info['in_library'] @@ -164,15 +162,14 @@ class ShowBase(MediaBase): season_info = seasons_info[season_nr] episodes = season_info.get('episodes', {}) - season = fireEvent('show.season.add', m.get('_id'), season_info, single = True) + season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True) # Add Episodes for episode_nr in episodes: episode_info = episodes[episode_nr] episode_info['season_number'] = season_nr - fireEvent('show.episode.add', season.get('_id'), episode_info, single = True) - + fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) if added and notify_after: @@ -191,10 +188,7 @@ class ShowBase(MediaBase): except: log.error('Failed adding media: %s', traceback.format_exc()) - def updateInfo(self, media_id = None, identifiers = None, info = None): - if not info: info = {} - if not identifiers: identifiers = {} - + def updateInfo(self, media_id = None, media = None, identifiers = None, info = None): """ Update movie information inside media['doc']['info'] @@ -209,29 +203,38 @@ class ShowBase(MediaBase): @return: dict, with media """ + if not info: info = {} + if not identifiers: identifiers = {} + if self.shuttingDown(): return try: db = get_db() - if media_id: - media = db.get('id', media_id) - else: - media = db.get('media', identifiers, with_doc = True)['doc'] + if media is None: + if media_id: + media = db.get('id', media_id) + else: + media = db.get('media', identifiers, with_doc = True)['doc'] if not info: info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) - # Don't need those here - try: del info['seasons'] - except: pass + # Remove season info for later use (save separately) + seasons_info = info.get('seasons', {}) + identifiers = info.get('identifiers', {}) + try: del info['identifiers'] except: pass 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 if not info or len(info) == 0: log.error('Could not update, no show info to work with: %s', media.get('identifier')) @@ -240,9 +243,35 @@ class ShowBase(MediaBase): # Update basic info media['info'] = info + show_tree = fireEvent('library.tree', media_id = media['_id'], single = True) + + # Update seasons + for season_num in seasons_info: + season_info = seasons_info[season_num] + episodes = season_info.get('episodes', {}) + + # Find season that matches number + season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', [])) + + if not season: + log.warning('Unable to find season "%s"', season_num) + continue + + # Update season + fireEvent('show.season.update_info', season['_id'], info = season_info, single = True) + + # Update episodes + for episode_num in episodes: + episode_info = episodes[episode_num] + episode_info['season_number'] = season_num + + # Find episode that matches number + episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', [])) + + fireEvent('show.episode.update_info', episode['_id'], info = episode_info, single = True) + # Update image file image_urls = info.get('images', []) - self.getPoster(media, image_urls) db.update(media) diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index 754a48f..0993bfd 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -48,13 +48,17 @@ class Episode(MediaBase): # 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'), info = info, single = True) + handle('show.season.update_info', episode.get('_id'), identifiers, info, single = True) return episode - def updateInfo(self, media_id = None, info = None, force = False): + def updateInfo(self, media_id = None, identifiers = None, info = None): if not info: info = {} + identifiers = info.get('identifiers') or identifiers + try: del info['identifiers'] + except: pass + if self.shuttingDown(): return @@ -69,26 +73,25 @@ class Episode(MediaBase): info = fireEvent( 'episode.info', show.get('identifiers'), { - 'season_identifier': season.get('info', {}).get('number'), - 'episode_identifier': episode.get('identifiers') + 'season_identifiers': season.get('identifiers'), + 'season_number': season.get('info', {}).get('number'), + + 'episode_identifiers': episode.get('identifiers'), + 'episode_number': episode.get('info', {}).get('number'), + + 'absolute_number': episode.get('info', {}).get('absolute_number') }, merge = True ) # Update/create media - if force: - - episode['identifiers'].update(info['identifiers']) - if 'identifiers' in info: - del info['identifiers'] - - episode.update({'info': info}) - e = db.update(episode) - episode.update(e) + episode['identifiers'].update(identifiers) + episode.update({'info': info}) # Get images image_urls = info.get('images', []) existing_files = episode.get('files', {}) self.getPoster(image_urls, existing_files) + db.update(episode) return episode diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index 6c9ff09..eb5fd6a 100755 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -50,38 +50,45 @@ class Season(MediaBase): # Update library info if update_after is not False: handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update_info', season.get('_id'), info = info, single = True) + handle('show.season.update_info', season.get('_id'), identifiers, info, single = True) return season - def updateInfo(self, media_id = None, info = None, force = False): + def updateInfo(self, media_id = None, identifiers = None, info = None): if not info: info = {} + identifiers = info.get('identifiers') or identifiers + try: del info['identifiers'] + except: pass + try: del info['episodes'] + except: pass + if self.shuttingDown(): return db = get_db() - season = db.get('id', media_id) + if media_id: + season = db.get('id', media_id) + else: + season = db.get('media', identifiers, with_doc = True)['doc'] + + show = db.get('id', season['parent_id']) # Get new info if not info: - info = fireEvent('season.info', season.get('identifiers'), merge = True) + info = fireEvent('season.info', show.get('identifiers'), { + 'season_number': season.get('info', {}).get('number', 0) + }, merge = True) # Update/create media - if force: - - season['identifiers'].update(info['identifiers']) - if 'identifiers' in info: - del info['identifiers'] - - season.update({'info': info}) - s = db.update(season) - season.update(s) + season['identifiers'].update(identifiers) + season.update({'info': info}) # Get images image_urls = info.get('images', []) existing_files = season.get('files', {}) self.getPoster(image_urls, existing_files) + db.update(season) return season From 482f5f82e64688f7667451248df36718e934d341 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:12:31 +1200 Subject: [PATCH 31/59] [TV] Disable tvdb query simplifying (API doesn't support "fuzzy" matching) --- couchpotato/core/media/show/providers/info/thetvdb.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index e57057c..7cbe3f4 100755 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -60,8 +60,9 @@ class TheTVDb(ShowProvider): self.tvdb_api_parms['language'] = language self._setup() - search_string = simplifyString(q) - cache_key = 'thetvdb.cache.search.%s.%s' % (search_string, limit) + query = q + #query = simplifyString(query) + cache_key = 'thetvdb.cache.search.%s.%s' % (query, limit) results = self.getCache(cache_key) if not results: @@ -69,9 +70,9 @@ class TheTVDb(ShowProvider): raw = None try: - raw = self.tvdb.search(search_string) + raw = self.tvdb.search(query) except (tvdb_exceptions.tvdb_error, IOError), e: - log.error('Failed searching TheTVDB for "%s": %s', (search_string, traceback.format_exc())) + log.error('Failed searching TheTVDB for "%s": %s', (query, traceback.format_exc())) return False results = [] From 8018ef979ff291564bc90abdcb35de645a341723 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:13:13 +1200 Subject: [PATCH 32/59] [TV] Fixes to TheTVDb.getSeasonInfo --- .../core/media/show/providers/info/thetvdb.py | 39 ++++++++++------------ 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index 7cbe3f4..4aa989c 100755 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -130,22 +130,17 @@ class TheTVDb(ShowProvider): return result or {} - def getSeasonInfo(self, identifier = None, params = {}): + def getSeasonInfo(self, identifiers = 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) + if not identifiers or not identifiers.get('thetvdb'): + return 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: season_identifier = int(season_identifier.split(':')[1]) - except: return False + season_number = params.get('season_number', None) + identifier = tryInt(identifiers.get('thetvdb')) - cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_identifier) + cache_key = 'thetvdb.cache.%s.%s' % (identifier, season_number) log.debug('Getting SeasonInfo: %s', cache_key) result = self.getCache(cache_key) or {} if result: @@ -159,12 +154,12 @@ class TheTVDb(ShowProvider): result = [] for number, season in show.items(): - if season_identifier is not None and number == season_identifier: - result = self._parseSeason(show, (number, season)) + if season_number is not None and number == season_number: + result = self._parseSeason(show, number, season) self.setCache(cache_key, result) return result else: - result.append(self._parseSeason(show, (number, season))) + result.append(self._parseSeason(show, number, season)) self.setCache(cache_key, result) return result @@ -173,22 +168,22 @@ class TheTVDb(ShowProvider): """Either return a list of all episodes or a single episode. If episode_identifer contains an episode number to search for """ - season_identifier = self.getIdentifier(params.get('season_identifier', None)) - episode_identifier = self.getIdentifier(params.get('episode_identifier', None)) + season_number = self.getIdentifier(params.get('season_number', None)) + episode_identifier = self.getIdentifier(params.get('episode_identifiers', None)) identifier = self.getIdentifier(identifier) - if not identifier and season_identifier is None: + if not identifier and season_number is None: return False # 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 not identifier and season_identifier: + if not identifier and season_number: try: - identifier, season_identifier = season_identifier.split(':') - season_identifier = int(season_identifier) + identifier, season_number = season_number.split(':') + season_number = int(season_number) except: return None - cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_identifier) + cache_key = 'thetvdb.cache.%s.%s.%s' % (identifier, episode_identifier, season_number) log.debug('Getting EpisodeInfo: %s', cache_key) result = self.getCache(cache_key) or {} if result: @@ -202,7 +197,7 @@ class TheTVDb(ShowProvider): result = [] for number, season in show.items(): - if season_identifier is not None and number != season_identifier: + if season_number is not None and number != season_number: continue for episode in season.values(): From 02d4a7625b7e7d95b743ae13b9fb37140f0e8187 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 16:14:05 +1200 Subject: [PATCH 33/59] [TV] Fixes to xem info provider, updated data structure --- couchpotato/core/media/show/providers/info/xem.py | 112 ++++++++++++++-------- 1 file changed, 72 insertions(+), 40 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/providers/info/xem.py diff --git a/couchpotato/core/media/show/providers/info/xem.py b/couchpotato/core/media/show/providers/info/xem.py old mode 100644 new mode 100755 index ec7d343..807184d --- a/couchpotato/core/media/show/providers/info/xem.py +++ b/couchpotato/core/media/show/providers/info/xem.py @@ -5,6 +5,8 @@ from couchpotato.core.media.show.providers.base import ShowProvider log = CPLog(__name__) +autoload = 'Xem' + class Xem(ShowProvider): ''' @@ -76,75 +78,78 @@ class Xem(ShowProvider): 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): + def getShowInfo(self, identifiers = None): if self.isDisabled(): return {} + identifier = identifiers.get('thetvdb') + + if not identifier: + return {} + cache_key = 'xem.cache.%s' % identifier log.debug('Getting showInfo: %s', cache_key) result = self.getCache(cache_key) or {} if result: return result + result['seasons'] = {} + # Create season/episode and absolute mappings - url = self.config['url_all'] + "id=%s&origin=tvdb" % tryUrlencode(identifier) + 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) + + if response and response.get('result') == 'success': + data = response.get('data', None) + self.parseMaps(result, data) # Create name alias mappings - url = self.config['url_names'] + "id=%s&origin=tvdb" % tryUrlencode(identifier) + 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}) + + if response and response.get('result') == 'success': + data = response.get('data', None) + self.parseNames(result, data) self.setCache(cache_key, result) return result - def getEpisodeInfo(self, identifier = None, params = {}): - episode = params.get('episode', None) - if episode is None: + def getEpisodeInfo(self, identifiers = None, params = {}): + episode_number = params.get('episode_number', None) + if episode_number is None: return False - season_identifier = params.get('season_identifier', None) - if season_identifier is None: + season_number = params.get('season_number', None) + if season_number 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 + absolute_number = params.get('absolute_number', None) + episode_identifier = params.get('episode_identifiers', {}).get('thetvdb') - result = self.getShowInfo(identifier) + result = self.getShowInfo(identifiers) map = {} + if result: - map_episode = result.get('map_episode', {}).get(season, {}).get(episode, {}) + map_episode = result.get('map_episode', {}).get(season_number, {}).get(episode_number, {}) + if map_episode: map.update({'map_episode': map_episode}) - if absolute: - map_absolute = result.get('map_absolute', {}).get(absolute, {}) + if absolute_number: + map_absolute = result.get('map_absolute', {}).get(absolute_number, {}) + if map_absolute: map.update({'map_absolute': map_absolute}) - map_names = result.get('map_names', {}).get(toUnicode(season), {}) + map_names = result.get('map_names', {}).get(toUnicode(season_number), {}) + if map_names: map.update({'map_names': map_names}) return map - def _parse(self, data, master = 'tvdb'): + def parseMaps(self, result, data, master = 'tvdb'): '''parses xem map and returns a custom formatted dict map To retreive map for scene: @@ -152,17 +157,44 @@ class Xem(ShowProvider): print map['map_episode'][1][1]['scene']['season'] ''' if not isinstance(data, list): - return {} + return - map = {'map_episode': {}, 'map_absolute': {}} - for maps in data: - origin = maps.pop(master, None) + for episode_map in data: + origin = episode_map.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()) + continue # No master origin to map to - return map + o_season = origin['season'] + o_episode = origin['episode'] + + # Create season info + if o_season not in result['seasons']: + result['seasons'][o_season] = {} + + season = result['seasons'][o_season] + + if 'episodes' not in season: + season['episodes'] = {} + + # Create episode info + if o_episode not in season['episodes']: + season['episodes'][o_episode] = {} + + episode = season['episodes'][o_episode] + episode['episode_map'] = episode_map + + def parseNames(self, result, data): + result['title_map'] = data.pop('all', None) + + for season, title_map in data.items(): + season = int(season) + + # Create season info + if season not in result['seasons']: + result['seasons'][season] = {} + + season = result['seasons'][season] + season['title_map'] = title_map def isDisabled(self): if __name__ == '__main__': From 5c4f8186dfebde72131296731cd3fc8cebc00a7e Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 19:45:10 +1200 Subject: [PATCH 34/59] [TV] Restructured and cleaned "show.add" and "show.update_info" --- couchpotato/core/media/show/_base/main.py | 343 +++++++++++++++--------------- couchpotato/core/media/show/episode.py | 8 +- 2 files changed, 181 insertions(+), 170 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 7da66ae..55a91cb 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -33,6 +33,7 @@ class ShowBase(MediaBase): addEvent('show.add', self.add) addEvent('show.update_info', self.updateInfo) + addEvent('show.update_extras', self.updateExtras) def addView(self, **kwargs): add_dict = self.add(params = kwargs) @@ -45,8 +46,6 @@ class ShowBase(MediaBase): 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.' @@ -58,121 +57,13 @@ class ShowBase(MediaBase): 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 - } - - # Remove season info for later use (save separately) - seasons_info = info.get('seasons', {}) - identifiers = info.get('identifiers', {}) - - # Make sure we don't nest in_wanted data - try: del info['identifiers'] - except: pass - 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 = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, single = 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 fireEvent('release.for_media', m['_id'], single = True): - 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'], info = info, on_complete = onComplete) - - # Remove releases - for rel in fireEvent('release.for_media', m['_id'], single = True): - if rel['status'] is 'available': - db.delete(rel) - - movie_dict = fireEvent('media.get', m['_id'], single = True) - - if do_search and search_after: - onComplete = self.createOnComplete(m['_id']) - onComplete() - - # Add Seasons - for season_nr in seasons_info: - - season_info = seasons_info[season_nr] - episodes = season_info.get('episodes', {}) + m, added = self.create(info, params, force_readd, search_after, update_after) - season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True) - - # Add Episodes - for episode_nr in episodes: - - episode_info = episodes[episode_nr] - episode_info['season_number'] = season_nr - fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) + result = fireEvent('media.get', m['_id'], single = True) if added and notify_after: - if params.get('title'): message = 'Successfully added "%s" to your wanted list.' % params.get('title', '') else: @@ -181,10 +72,10 @@ class ShowBase(MediaBase): message = 'Successfully added "%s" to your wanted list.' % title else: message = 'Successfully added to your wanted list.' - fireEvent('notify.frontend', type = 'show.added', data = movie_dict, message = message) + fireEvent('notify.frontend', type = 'show.added', data = result, message = message) - return movie_dict + return result except: log.error('Failed adding media: %s', traceback.format_exc()) @@ -206,77 +97,195 @@ class ShowBase(MediaBase): if not info: info = {} if not identifiers: identifiers = {} + db = get_db() + if self.shuttingDown(): return + if media is None and media_id: + media = db.get('id', media_id) + else: + log.error('missing "media" and "media_id" parameters, unable to update') + return + + if not info: + info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) + try: - db = get_db() + identifiers = info.pop('identifiers', {}) + seasons = info.pop('seasons', {}) - if media is None: - if media_id: - media = db.get('id', media_id) - else: - media = db.get('media', identifiers, with_doc = True)['doc'] + self.update(media, info) + self.updateEpisodes(media, seasons) + self.updateExtras(media, info) - if not info: - info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) + db.update(media) + return media + except: + log.error('Failed update media: %s', traceback.format_exc()) - # Remove season info for later use (save separately) - seasons_info = info.get('seasons', {}) - identifiers = info.get('identifiers', {}) + return {} - try: del info['identifiers'] - except: pass - 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 + def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): + db = get_db() - if not info or len(info) == 0: - log.error('Could not update, no show info to work with: %s', media.get('identifier')) - return False + # Set default title + def_title = self.getDefaultTitle(info) - # Update basic info - media['info'] = info + # Default profile and category + default_profile = {} + if not params.get('profile_id'): + default_profile = fireEvent('profile.default', single = True) - show_tree = fireEvent('library.tree', media_id = media['_id'], single = True) + cat_id = params.get('category_id') - # Update seasons - for season_num in seasons_info: - season_info = seasons_info[season_num] - episodes = season_info.get('episodes', {}) + 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 + } - # Find season that matches number - season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', [])) + identifiers = info.pop('identifiers', {}) + seasons = info.pop('seasons', {}) - if not season: - log.warning('Unable to find season "%s"', season_num) - continue + # Update media with info + self.update(media, info) - # Update season - fireEvent('show.season.update_info', season['_id'], info = season_info, single = True) + new = False + try: + m = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, single = True)['doc'] + except: + new = True + m = db.insert(media) - # Update episodes - for episode_num in episodes: - episode_info = episodes[episode_num] - episode_info['season_number'] = season_num + # Update dict to be usable + m.update(media) - # Find episode that matches number - episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', [])) + added = True + do_search = False + search_after = search_after and self.conf('search_on_add', section = 'showsearcher') + onComplete = None - fireEvent('show.episode.update_info', episode['_id'], info = episode_info, single = True) + if new: + if search_after: + onComplete = self.createOnComplete(m['_id']) - # Update image file - image_urls = info.get('images', []) - self.getPoster(media, image_urls) + search_after = False + elif force_readd: + # Clean snatched history + for release in fireEvent('release.for_media', m['_id'], single = True): + 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) - db.update(media) - return media - except: - log.error('Failed update media: %s', traceback.format_exc()) + m['profile_id'] = params.get('profile_id', default_profile.get('id')) + m['category_id'] = media.get('category_id') + m['last_edit'] = int(time.time()) - return {} + do_search = True + db.update(m) + else: + params.pop('info', None) + log.debug('Show already exists, not updating: %s', params) + added = False + + # Create episodes + self.createEpisodes(m, seasons) + + # Trigger update info + if added and update_after: + # Do full update to get images etc + fireEventAsync('show.update_extras', m, info = info, store = True, on_complete = onComplete) + + # Remove releases + for rel in fireEvent('release.for_media', m['_id'], single = True): + if rel['status'] is 'available': + db.delete(rel) + + if do_search and search_after: + onComplete = self.createOnComplete(m['_id']) + onComplete() + + return m, added + + def createEpisodes(self, m, seasons_info): + # Add Seasons + for season_nr in seasons_info: + season_info = seasons_info[season_nr] + episodes = season_info.get('episodes', {}) + + season = fireEvent('show.season.add', m.get('_id'), season_info, update_after = False, single = True) + + # Add Episodes + for episode_nr in episodes: + episode_info = episodes[episode_nr] + episode_info['season_number'] = season_nr + + fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) + + def update(self, media, info): + db = get_db() + + # Remove season info for later use (save separately) + info.pop('in_wanted', None) + info.pop('in_library', None) + + 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 + + def updateEpisodes(self, media, seasons): + # Fetch current season/episode tree + show_tree = fireEvent('library.tree', media_id = media['_id'], single = True) + + # Update seasons + for season_num in seasons: + season_info = seasons[season_num] + episodes = season_info.get('episodes', {}) + + # Find season that matches number + season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', [])) + + if not season: + log.warning('Unable to find season "%s"', season_num) + continue + + # Update season + fireEvent('show.season.update_info', season['_id'], info = season_info, single = True) + + # Update episodes + for episode_num in episodes: + episode_info = episodes[episode_num] + episode_info['season_number'] = season_num + + # Find episode that matches number + episode = find(lambda s: s.get('info', {}).get('number', 0) == episode_num, season.get('episodes', [])) + + if not episode: + log.debug('Creating new episode %s in season %s', (episode_num, season_num)) + fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) + continue + + fireEvent('show.episode.update_info', episode['_id'], info = episode_info, single = True) + + def updateExtras(self, media, info, store=False): + log.debug('Updating extras for "%s"', media['_id']) + + db = get_db() + + # Update image file + image_urls = info.get('images', []) + self.getPoster(media, image_urls) + + if store: + db.update(media) diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index 0993bfd..d849486 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -19,9 +19,11 @@ class Episode(MediaBase): def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} - identifiers = info.get('identifiers') - try: del info['identifiers'] - except: pass + identifiers = info.pop('identifiers', None) + + if not identifiers: + log.warning('Unable to add episode, missing identifiers (info provider mismatch?)') + return # Add Season episode_info = { From b10e25ab8c814d2e5ba983f3ea3fb82e065743c3 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 19:45:45 +1200 Subject: [PATCH 35/59] [TV] Disabled excessive logging from tvdb_api --- libs/tvdb_api/tvdb_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 libs/tvdb_api/tvdb_api.py diff --git a/libs/tvdb_api/tvdb_api.py b/libs/tvdb_api/tvdb_api.py old mode 100644 new mode 100755 index 4bfe78a..87e8336 --- a/libs/tvdb_api/tvdb_api.py +++ b/libs/tvdb_api/tvdb_api.py @@ -705,7 +705,7 @@ class Tvdb: for k, v in banners[btype][btype2][bid].items(): if k.endswith("path"): new_key = "_%s" % (k) - log().debug("Transforming %s to %s" % (k, new_key)) + #log().debug("Transforming %s to %s" % (k, new_key)) new_url = self.config['url_artworkPrefix'] % (v) banners[btype][btype2][bid][new_key] = new_url From 212d5c543257ab402c3cf70833ddd8f36dee5593 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 19:54:06 +1200 Subject: [PATCH 36/59] Renamed "[media].update_info" event to "[media].update" --- couchpotato/core/media/_base/media/main.py | 2 +- couchpotato/core/media/movie/_base/main.py | 8 ++++---- couchpotato/core/media/movie/providers/metadata/base.py | 2 +- couchpotato/core/media/movie/searcher.py | 2 +- couchpotato/core/plugins/manage.py | 2 +- couchpotato/core/plugins/renamer.py | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) mode change 100644 => 100755 couchpotato/core/media/_base/media/main.py mode change 100644 => 100755 couchpotato/core/media/movie/providers/metadata/base.py mode change 100644 => 100755 couchpotato/core/media/movie/searcher.py mode change 100644 => 100755 couchpotato/core/plugins/manage.py mode change 100644 => 100755 couchpotato/core/plugins/renamer.py diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py old mode 100644 new mode 100755 index ee9e6cc..a5a1bbd --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -109,7 +109,7 @@ class MediaPlugin(MediaBase): try: media = get_db().get('id', media_id) - event = '%s.update_info' % media.get('type') + event = '%s.update' % media.get('type') def handler(): fireEvent(event, media_id = media_id, on_complete = self.createOnComplete(media_id)) diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 3afe1e8..8a04d0b 100755 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -46,7 +46,7 @@ class MovieBase(MovieTypeBase): }) addEvent('movie.add', self.add) - addEvent('movie.update_info', self.updateInfo) + addEvent('movie.update', self.update) addEvent('movie.update_release_dates', self.updateReleaseDate) def add(self, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): @@ -172,7 +172,7 @@ class MovieBase(MovieTypeBase): # Trigger update info if added and update_after: # Do full update to get images etc - fireEventAsync('movie.update_info', m['_id'], default_title = params.get('title'), on_complete = onComplete) + fireEventAsync('movie.update', m['_id'], default_title = params.get('title'), on_complete = onComplete) # Remove releases for rel in fireEvent('release.for_media', m['_id'], single = True): @@ -256,7 +256,7 @@ class MovieBase(MovieTypeBase): 'success': False, } - def updateInfo(self, media_id = None, identifier = None, default_title = None, extended = False): + def update(self, media_id = None, identifier = None, default_title = None, extended = False): """ Update movie information inside media['doc']['info'] @@ -337,7 +337,7 @@ class MovieBase(MovieTypeBase): media = db.get('id', media_id) if not media.get('info'): - media = self.updateInfo(media_id) + media = self.update(media_id) dates = media.get('info', {}).get('release_date') else: dates = media.get('info').get('release_date') diff --git a/couchpotato/core/media/movie/providers/metadata/base.py b/couchpotato/core/media/movie/providers/metadata/base.py old mode 100644 new mode 100755 index 7968000..cc914af --- a/couchpotato/core/media/movie/providers/metadata/base.py +++ b/couchpotato/core/media/movie/providers/metadata/base.py @@ -28,7 +28,7 @@ class MovieMetaData(MetaDataBase): # Update library to get latest info try: - group['media'] = fireEvent('movie.update_info', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True) + group['media'] = fireEvent('movie.update', group['media'].get('_id'), identifier = getIdentifier(group['media']), extended = True, single = True) except: log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc()) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py old mode 100644 new mode 100755 index 4bd8c8d..e943c21 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -94,7 +94,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.single(media, search_protocols, manual = manual) except IndexError: log.error('Forcing library update for %s, if you see this often, please report: %s', (getIdentifier(media), traceback.format_exc())) - fireEvent('movie.update_info', media_id) + fireEvent('movie.update', media_id) except: log.error('Search failed for %s: %s', (getIdentifier(media), traceback.format_exc())) diff --git a/couchpotato/core/plugins/manage.py b/couchpotato/core/plugins/manage.py old mode 100644 new mode 100755 index c8d53ea..d389e22 --- a/couchpotato/core/plugins/manage.py +++ b/couchpotato/core/plugins/manage.py @@ -219,7 +219,7 @@ class Manage(Plugin): # Add it to release and update the info fireEvent('release.add', group = group, update_info = False) - fireEvent('movie.update_info', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier'])) + fireEvent('movie.update', identifier = group['identifier'], on_complete = self.createAfterUpdate(folder, group['identifier'])) return addToLibrary diff --git a/couchpotato/core/plugins/renamer.py b/couchpotato/core/plugins/renamer.py old mode 100644 new mode 100755 index 9f6792a..b331ec8 --- a/couchpotato/core/plugins/renamer.py +++ b/couchpotato/core/plugins/renamer.py @@ -247,7 +247,7 @@ class Renamer(Plugin): 'profile_id': None }, search_after = False, status = 'done', single = True) else: - group['media'] = fireEvent('movie.update_info', media_id = group['media'].get('_id'), single = True) + group['media'] = fireEvent('movie.update', media_id = group['media'].get('_id'), single = True) if not group['media'] or not group['media'].get('_id'): log.error('Could not rename, no library item to work with: %s', group_identifier) From 90be6ec38ba171c3e45a7a95bcc865c56c16fed7 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 19:55:09 +1200 Subject: [PATCH 37/59] [TV] Renamed "[media].update_info" events, renamed "updateInfo" functions --- couchpotato/core/media/show/_base/main.py | 106 +++++++++++++++--------------- couchpotato/core/media/show/episode.py | 6 +- couchpotato/core/media/show/season.py | 6 +- 3 files changed, 58 insertions(+), 60 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 55a91cb..f883cb1 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -32,7 +32,7 @@ class ShowBase(MediaBase): }) addEvent('show.add', self.add) - addEvent('show.update_info', self.updateInfo) + addEvent('show.update', self.update) addEvent('show.update_extras', self.updateExtras) def addView(self, **kwargs): @@ -79,53 +79,6 @@ class ShowBase(MediaBase): except: log.error('Failed adding media: %s', traceback.format_exc()) - def updateInfo(self, media_id = None, media = None, identifiers = None, info = 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 not info: info = {} - if not identifiers: identifiers = {} - - db = get_db() - - if self.shuttingDown(): - return - - if media is None and media_id: - media = db.get('id', media_id) - else: - log.error('missing "media" and "media_id" parameters, unable to update') - return - - if not info: - info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) - - try: - identifiers = info.pop('identifiers', {}) - seasons = info.pop('seasons', {}) - - self.update(media, info) - self.updateEpisodes(media, seasons) - self.updateExtras(media, info) - - db.update(media) - return media - except: - log.error('Failed update media: %s', traceback.format_exc()) - - return {} - def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): db = get_db() @@ -153,7 +106,7 @@ class ShowBase(MediaBase): seasons = info.pop('seasons', {}) # Update media with info - self.update(media, info) + self.updateInfo(media, info) new = False try: @@ -230,7 +183,54 @@ class ShowBase(MediaBase): fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) - def update(self, media, info): + def update(self, media_id = None, media = None, identifiers = None, info = 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 not info: info = {} + if not identifiers: identifiers = {} + + db = get_db() + + if self.shuttingDown(): + return + + if media is None and media_id: + media = db.get('id', media_id) + else: + log.error('missing "media" and "media_id" parameters, unable to update') + return + + if not info: + info = fireEvent('show.info', identifiers = media.get('identifiers'), merge = True) + + try: + identifiers = info.pop('identifiers', {}) + seasons = info.pop('seasons', {}) + + self.updateInfo(media, info) + self.updateEpisodes(media, seasons) + self.updateExtras(media, info) + + db.update(media) + return media + except: + log.error('Failed update media: %s', traceback.format_exc()) + + return {} + + def updateInfo(self, media, info): db = get_db() # Remove season info for later use (save separately) @@ -261,7 +261,7 @@ class ShowBase(MediaBase): continue # Update season - fireEvent('show.season.update_info', season['_id'], info = season_info, single = True) + fireEvent('show.season.update', season['_id'], info = season_info, single = True) # Update episodes for episode_num in episodes: @@ -276,11 +276,9 @@ class ShowBase(MediaBase): fireEvent('show.episode.add', season.get('_id'), episode_info, update_after = False, single = True) continue - fireEvent('show.episode.update_info', episode['_id'], info = episode_info, single = True) + fireEvent('show.episode.update', episode['_id'], info = episode_info, single = True) def updateExtras(self, media, info, store=False): - log.debug('Updating extras for "%s"', media['_id']) - db = get_db() # Update image file diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index d849486..4b07b38 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -14,7 +14,7 @@ class Episode(MediaBase): def __init__(self): addEvent('show.episode.add', self.add) - addEvent('show.episode.update_info', self.updateInfo) + addEvent('show.episode.update', self.update) def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} @@ -50,11 +50,11 @@ class Episode(MediaBase): # 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'), identifiers, info, single = True) + handle('show.season.update', episode.get('_id'), identifiers, info, single = True) return episode - def updateInfo(self, media_id = None, identifiers = None, info = None): + def update(self, media_id = None, identifiers = None, info = None): if not info: info = {} identifiers = info.get('identifiers') or identifiers diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index eb5fd6a..e85720c 100755 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -14,7 +14,7 @@ class Season(MediaBase): def __init__(self): addEvent('show.season.add', self.add) - addEvent('show.season.update_info', self.updateInfo) + addEvent('show.season.update', self.update) def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} @@ -50,11 +50,11 @@ class Season(MediaBase): # Update library info if update_after is not False: handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update_info', season.get('_id'), identifiers, info, single = True) + handle('show.season.update', season.get('_id'), identifiers, info, single = True) return season - def updateInfo(self, media_id = None, identifiers = None, info = None): + def update(self, media_id = None, identifiers = None, info = None): if not info: info = {} identifiers = info.get('identifiers') or identifiers From 72cb53bcc0fa9f3354c79c50c8edc7e84cef0fd6 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 20:19:51 +1200 Subject: [PATCH 38/59] [TV] Fixed xem episode updates and finished adding "update_extras" events --- couchpotato/core/media/show/_base/main.py | 2 +- couchpotato/core/media/show/episode.py | 22 ++++++++----- couchpotato/core/media/show/providers/info/xem.py | 38 +++++++++-------------- couchpotato/core/media/show/season.py | 38 +++++++++++------------ 4 files changed, 49 insertions(+), 51 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index f883cb1..29af63a 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -155,7 +155,7 @@ class ShowBase(MediaBase): # Trigger update info if added and update_after: # Do full update to get images etc - fireEventAsync('show.update_extras', m, info = info, store = True, on_complete = onComplete) + fireEventAsync('show.update_extras', m, info, store = True, on_complete = onComplete) # Remove releases for rel in fireEvent('release.for_media', m['_id'], single = True): diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py index 4b07b38..0557bb4 100755 --- a/couchpotato/core/media/show/episode.py +++ b/couchpotato/core/media/show/episode.py @@ -15,6 +15,7 @@ class Episode(MediaBase): def __init__(self): addEvent('show.episode.add', self.add) addEvent('show.episode.update', self.update) + addEvent('show.episode.update_extras', self.updateExtras) def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} @@ -43,6 +44,7 @@ class Episode(MediaBase): if existing_episode: s = existing_episode['doc'] s.update(episode_info) + episode = db.update(s) else: episode = db.insert(episode_info) @@ -50,17 +52,13 @@ class Episode(MediaBase): # Update library info if update_after is not False: handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update', episode.get('_id'), identifiers, info, single = True) + handle('show.episode.update_extras', episode, info, store = True, single = True) return episode def update(self, media_id = None, identifiers = None, info = None): if not info: info = {} - identifiers = info.get('identifiers') or identifiers - try: del info['identifiers'] - except: pass - if self.shuttingDown(): return @@ -86,14 +84,24 @@ class Episode(MediaBase): merge = True ) + identifiers = info.pop('identifiers', None) or identifiers + # Update/create media episode['identifiers'].update(identifiers) episode.update({'info': info}) + self.updateExtras(episode, info) + + db.update(episode) + return episode + + def updateExtras(self, episode, info, store=False): + db = get_db() + # Get images image_urls = info.get('images', []) existing_files = episode.get('files', {}) self.getPoster(image_urls, existing_files) - db.update(episode) - return episode + if store: + db.update(episode) diff --git a/couchpotato/core/media/show/providers/info/xem.py b/couchpotato/core/media/show/providers/info/xem.py index 807184d..f8c003c 100755 --- a/couchpotato/core/media/show/providers/info/xem.py +++ b/couchpotato/core/media/show/providers/info/xem.py @@ -77,7 +77,6 @@ class Xem(ShowProvider): 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, identifiers = None): if self.isDisabled(): return {} @@ -115,39 +114,30 @@ class Xem(ShowProvider): return result def getEpisodeInfo(self, identifiers = None, params = {}): - episode_number = params.get('episode_number', None) - if episode_number is None: + episode_num = params.get('episode_number', None) + if episode_num is None: return False - season_number = params.get('season_number', None) - if season_number is None: + season_num = params.get('season_number', None) + if season_num is None: return False - absolute_number = params.get('absolute_number', None) - episode_identifier = params.get('episode_identifiers', {}).get('thetvdb') - result = self.getShowInfo(identifiers) - map = {} - - if result: - map_episode = result.get('map_episode', {}).get(season_number, {}).get(episode_number, {}) - - if map_episode: - map.update({'map_episode': map_episode}) - if absolute_number: - map_absolute = result.get('map_absolute', {}).get(absolute_number, {}) - - if map_absolute: - map.update({'map_absolute': map_absolute}) + if not result: + return False - map_names = result.get('map_names', {}).get(toUnicode(season_number), {}) + # Find season + if season_num not in result['seasons']: + return False - if map_names: - map.update({'map_names': map_names}) + season = result['seasons'][season_num] - return map + # Find episode + if episode_num not in season['episodes']: + return False + return season['episodes'][episode_num] def parseMaps(self, result, data, master = 'tvdb'): '''parses xem map and returns a custom formatted dict map diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py index e85720c..e41e460 100755 --- a/couchpotato/core/media/show/season.py +++ b/couchpotato/core/media/show/season.py @@ -15,15 +15,13 @@ class Season(MediaBase): def __init__(self): addEvent('show.season.add', self.add) addEvent('show.season.update', self.update) + addEvent('show.season.update_extras', self.updateExtras) def add(self, parent_id, info = None, update_after = True, status = None): if not info: info = {} - identifiers = info.get('identifiers') - try: del info['identifiers'] - except: pass - try: del info['episodes'] - except: pass + identifiers = info.pop('identifiers', None) + info.pop('episodes', None) # Add Season season_info = { @@ -43,6 +41,7 @@ class Season(MediaBase): if existing_season: s = existing_season['doc'] s.update(season_info) + season = db.update(s) else: season = db.insert(season_info) @@ -50,29 +49,19 @@ class Season(MediaBase): # Update library info if update_after is not False: handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update', season.get('_id'), identifiers, info, single = True) + handle('show.season.update_extras', season, info, store = True, single = True) return season def update(self, media_id = None, identifiers = None, info = None): if not info: info = {} - identifiers = info.get('identifiers') or identifiers - try: del info['identifiers'] - except: pass - try: del info['episodes'] - except: pass - if self.shuttingDown(): return db = get_db() - if media_id: - season = db.get('id', media_id) - else: - season = db.get('media', identifiers, with_doc = True)['doc'] - + season = db.get('id', media_id) show = db.get('id', season['parent_id']) # Get new info @@ -81,14 +70,25 @@ class Season(MediaBase): 'season_number': season.get('info', {}).get('number', 0) }, merge = True) + identifiers = info.pop('identifiers', None) or identifiers + info.pop('episodes', None) + # Update/create media season['identifiers'].update(identifiers) season.update({'info': info}) + self.updateExtras(season, info) + + db.update(season) + return season + + def updateExtras(self, season, info, store=False): + db = get_db() + # Get images image_urls = info.get('images', []) existing_files = season.get('files', {}) self.getPoster(image_urls, existing_files) - db.update(season) - return season + if store: + db.update(season) From fe2e508e4c54d04b76572f51f41e072aeceb6d1e Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Mon, 28 Jul 2014 20:33:50 +1200 Subject: [PATCH 39/59] Fix possible dashboard error, add "types" parameter to "media.with_status", limit suggestions to movies (for now) --- couchpotato/core/media/_base/media/main.py | 9 ++++++++- couchpotato/core/media/movie/suggestion/main.py | 2 +- couchpotato/core/plugins/dashboard.py | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) mode change 100644 => 100755 couchpotato/core/media/movie/suggestion/main.py diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index a5a1bbd..bcb8402 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -160,10 +160,13 @@ class MediaPlugin(MediaBase): 'media': media, } - def withStatus(self, status, with_doc = True): + def withStatus(self, status, types = None, with_doc = True): db = get_db() + if types and not isinstance(types, (list, tuple)): + types = [types] + status = list(status if isinstance(status, (list, tuple)) else [status]) for s in status: @@ -171,6 +174,10 @@ class MediaPlugin(MediaBase): if with_doc: try: doc = db.get('id', ms['_id']) + + if types and doc.get('type') not in types: + continue + yield doc except RecordNotFound: log.debug('Record not found, skipping: %s', ms['_id']) diff --git a/couchpotato/core/media/movie/suggestion/main.py b/couchpotato/core/media/movie/suggestion/main.py old mode 100644 new mode 100755 index 146a6a0..3df67ab --- a/couchpotato/core/media/movie/suggestion/main.py +++ b/couchpotato/core/media/movie/suggestion/main.py @@ -27,7 +27,7 @@ class Suggestion(Plugin): else: if not movies or len(movies) == 0: - active_movies = fireEvent('media.with_status', ['active', 'done'], single = True) + active_movies = fireEvent('media.with_status', ['active', 'done'], 'movie', single = True) movies = [getIdentifier(x) for x in active_movies] if not ignored or len(ignored) == 0: diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py index 1f5fdcd..0df828d 100755 --- a/couchpotato/core/plugins/dashboard.py +++ b/couchpotato/core/plugins/dashboard.py @@ -62,7 +62,7 @@ class Dashboard(Plugin): for media_id in active_ids: media = db.get('id', media_id) - pp = profile_pre.get(media['profile_id']) + pp = profile_pre.get(media.get('profile_id')) if not pp: continue eta = media['info'].get('release_date', {}) or {} From 3d6ce1c2e2c961881a664c8dff379c050c970009 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 17:26:07 +1200 Subject: [PATCH 40/59] [TV} Working show and season searcher, fixed season correctRelease/matcher --- couchpotato/core/media/show/matcher.py | 3 +- couchpotato/core/media/show/searcher/season.py | 142 ++++++++++++++++++++++--- couchpotato/core/media/show/searcher/show.py | 32 ++---- 3 files changed, 143 insertions(+), 34 deletions(-) diff --git a/couchpotato/core/media/show/matcher.py b/couchpotato/core/media/show/matcher.py index 006b48a..4137c1c 100755 --- a/couchpotato/core/media/show/matcher.py +++ b/couchpotato/core/media/show/matcher.py @@ -107,6 +107,7 @@ class Episode(Base): return True + class Season(Base): type = 'show.season' @@ -121,7 +122,7 @@ class Season(Base): log.info2('Wrong: releases with identifier ranges are not supported yet') return False - required = fireEvent('media.identifier', media['library'], single = True) + required = fireEvent('library.identifier', media, single = True) if identifier != required: log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py index b1fe630..6c1b5a4 100755 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -1,7 +1,9 @@ +from couchpotato import get_db, Env from couchpotato.api import addApiView -from couchpotato.core.event import addEvent, fireEventAsync +from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.logger import CPLog from couchpotato.core.media._base.searcher.base import SearcherBase +from couchpotato.core.media.movie.searcher import SearchSetupError from couchpotato.core.media.show import ShowTypeBase log = CPLog(__name__) @@ -19,6 +21,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): addEvent('%s.searcher.all' % self.getType(), self.searchAll) addEvent('%s.searcher.single' % self.getType(), self.single) + addEvent('searcher.correct_release', self.correctRelease) addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = { 'desc': 'Starts a full search for all wanted seasons', @@ -34,21 +37,136 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): def searchAll(self, manual = False): pass - def single(self, media, show, profile): + def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False): + db = get_db() + + related = fireEvent('library.related', media, single = True) + + # TODO search_protocols, profile, quality_order can be moved to a base method + # Find out search type + try: + if not search_protocols: + search_protocols = fireEvent('searcher.protocols', single = True) + except SearchSetupError: + return - # Check if any episode is already snatched - active = 0 - episodes = media.get('episodes', {}) - for ex in episodes: - episode = episodes.get(ex) + if not profile and related['show']['profile_id']: + profile = db.get('id', related['show']['profile_id']) + + if not quality_order: + quality_order = fireEvent('quality.order', single = True) - if episode.get('status') in ['active']: - active += 1 + # Find 'active' episodes + episodes = media.get('episodes', []) + episodes_active = [] - if active != len(episodes): + for episode in episodes: + if episode.get('status') != 'active': + continue + + episodes_active.append(episode) + + if len(episodes_active) == len(episodes): + # All episodes are 'active', try and search for full season + if self.search(media, profile, quality_order, search_protocols): + # Success, end season search + return True + else: + log.info('Unable to find season pack, searching for individual episodes...') + + # Search for each episode individually + for episode in episodes_active: + fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual) + + # TODO (testing) only grab one episode + return True + + return True + + def search(self, media, profile, quality_order, search_protocols): + # TODO: check episode status + # TODO: check air date + #if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']): + # too_early_to_search.append(quality_type['quality']['identifier']) + # return + + ret = False + has_better_quality = None + found_releases = [] + too_early_to_search = [] + + releases = fireEvent('release.for_media', media['_id'], single = True) + query = fireEvent('library.query', media, condense = False, single = True) + + index = 0 + for q_identifier in profile.get('qualities'): + quality_custom = { + 'quality': q_identifier, + 'finish': profile['finish'][index], + 'wait_for': profile['wait_for'][index], + '3d': profile['3d'][index] if profile.get('3d') else False + } + + has_better_quality = 0 + + # See if better quality is available + for release in releases: + if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']: + has_better_quality += 1 + + # Don't search for quality lower then already available. + if has_better_quality is 0: + + log.info('Searching for %s in %s', (query, q_identifier)) + quality = fireEvent('quality.single', identifier = q_identifier, single = True) + quality['custom'] = quality_custom + + results = fireEvent('searcher.search', search_protocols, media, quality, single = True) + if len(results) == 0: + log.debug('Nothing found for %s in %s', (query, q_identifier)) + + # Add them to this movie releases list + found_releases += fireEvent('release.create_from_search', results, media, quality, single = True) + + # Try find a valid result and download it + if fireEvent('release.try_download_result', results, media, quality, single = True): + ret = True + + # Remove releases that aren't found anymore + for release in releases: + if release.get('status') == 'available' and release.get('identifier') not in found_releases: + fireEvent('release.delete', release.get('id'), single = True) + else: + log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query)) + fireEvent('media.restatus', media['_id']) + break + + # Break if CP wants to shut down + if self.shuttingDown() or ret: + break + + if len(too_early_to_search) > 0: + log.info2('Too early to search for %s, %s', (too_early_to_search, query)) + + return len(found_releases) > 0 + + def correctRelease(self, release = None, media = None, quality = None, **kwargs): + if media.get('type') != 'show.season': + return + + retention = Env.setting('retention', section = 'nzb') + + if release.get('seeders') is None and 0 < retention < release.get('age', 0): + log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name'])) + return False + + # Check for required and ignored words + if not fireEvent('searcher.correct_words', release['name'], media, single = True): return False - # Try and search for full season - # TODO: + # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations) + match = fireEvent('matcher.match', release, media, quality, single = True) + if match: + return match.weight return False diff --git a/couchpotato/core/media/show/searcher/show.py b/couchpotato/core/media/show/searcher/show.py index 07d644b..b7619cc 100755 --- a/couchpotato/core/media/show/searcher/show.py +++ b/couchpotato/core/media/show/searcher/show.py @@ -54,36 +54,26 @@ class ShowSearcher(SearcherBase, ShowTypeBase): fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title) - media = self.extendShow(media) + show_tree = fireEvent('library.tree', media, single = True) db = get_db() profile = db.get('id', media['profile_id']) quality_order = fireEvent('quality.order', single = True) - seasons = media.get('seasons', {}) - for sx in seasons: + for season in show_tree.get('seasons', []): + if not season.get('info'): + continue - # Skip specials for now TODO: set status for specials to skipped by default - if sx == 0: continue + # Skip specials (and seasons missing 'number') for now + # TODO: set status for specials to skipped by default + if not season['info'].get('number'): + continue - season = seasons.get(sx) + # Check if full season can be downloaded + fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual) - # Check if full season can be downloaded TODO: add - season_success = fireEvent('show.season.searcher.single', season, media, profile) - - # Do each episode seperately - if not season_success: - episodes = season.get('episodes', {}) - for ex in episodes: - episode = episodes.get(ex) - - fireEvent('show.episode.searcher.single', episode, season, media, profile, quality_order, search_protocols) - - # TODO - return - - # TODO + # TODO (testing) only snatch one season return fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) From b9c6d983e12f0959c0681b9e168e9067f9773eb4 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 18:37:05 +1200 Subject: [PATCH 41/59] [TV] Added season actions/releases --- .../media/show/_base/static/episode.actions.js | 2 +- .../core/media/show/_base/static/episode.js | 6 +- couchpotato/core/media/show/_base/static/season.js | 127 +++++++++++++++++++++ couchpotato/core/media/show/_base/static/show.css | 59 +++++++++- .../core/media/show/_base/static/show.episodes.js | 16 +-- 5 files changed, 189 insertions(+), 21 deletions(-) create mode 100755 couchpotato/core/media/show/_base/static/season.js diff --git a/couchpotato/core/media/show/_base/static/episode.actions.js b/couchpotato/core/media/show/_base/static/episode.actions.js index fc2a0a2..cc46ce3 100755 --- a/couchpotato/core/media/show/_base/static/episode.actions.js +++ b/couchpotato/core/media/show/_base/static/episode.actions.js @@ -2,7 +2,7 @@ var EpisodeAction = new Class({ Implements: [Options], - class_name: 'action icon2', + class_name: 'item-action icon2', initialize: function(episode, options){ var self = this; diff --git a/couchpotato/core/media/show/_base/static/episode.js b/couchpotato/core/media/show/_base/static/episode.js index b7e73f3..92e49cc 100755 --- a/couchpotato/core/media/show/_base/static/episode.js +++ b/couchpotato/core/media/show/_base/static/episode.js @@ -14,7 +14,7 @@ var Episode = new Class({ self.profile = self.show.profile; - self.el = new Element('div.item').adopt( + self.el = new Element('div.item.episode').adopt( self.detail = new Element('div.item.data') ); @@ -34,14 +34,14 @@ var Episode = new Class({ self.quality = new Element('span.quality', { 'events': { 'click': function(e){ - var releases = self.detail.getElement('.episode-actions .releases'); + var releases = self.detail.getElement('.item-actions .releases'); if(releases.isVisible()) releases.fireEvent('click', [e]) } } }), - self.actions = new Element('div.episode-actions') + self.actions = new Element('div.item-actions') ); // Add profile diff --git a/couchpotato/core/media/show/_base/static/season.js b/couchpotato/core/media/show/_base/static/season.js new file mode 100755 index 0000000..0f1cf61 --- /dev/null +++ b/couchpotato/core/media/show/_base/static/season.js @@ -0,0 +1,127 @@ +var Season = new Class({ + + Extends: BlockBase, + + action: {}, + + initialize: function(show, options, data){ + var self = this; + self.setOptions(options); + + self.show = show; + self.options = options; + self.data = data; + + self.profile = self.show.profile; + + self.el = new Element('div.item.season').adopt( + self.detail = new Element('div.item.data') + ); + + self.create(); + }, + + create: function(){ + var self = this; + + self.detail.set('id', 'season_'+self.data._id); + + self.detail.adopt( + new Element('span.name', {'text': self.getTitle()}), + + self.quality = new Element('span.quality', { + 'events': { + 'click': function(e){ + var releases = self.detail.getElement('.item-actions .releases'); + + if(releases.isVisible()) + releases.fireEvent('click', [e]) + } + } + }), + self.actions = new Element('div.item-actions') + ); + + // Add profile + if(self.profile.data) { + self.profile.getTypes().each(function(type){ + var q = self.addQuality(type.get('quality'), type.get('3d')); + + if((type.finish == true || type.get('finish')) && !q.hasClass('finish')){ + q.addClass('finish'); + q.set('title', q.get('title') + ' Will finish searching for this movie if this quality is found.') + } + }); + } + + // Add releases + self.updateReleases(); + + Object.each(self.options.actions, function(action, key){ + self.action[key.toLowerCase()] = action = new self.options.actions[key](self); + if(action.el) + self.actions.adopt(action) + }); + }, + + updateReleases: function(){ + var self = this; + if(!self.data.releases || self.data.releases.length == 0) return; + + self.data.releases.each(function(release){ + + var q = self.quality.getElement('.q_'+ release.quality+(release.is_3d ? '.is_3d' : ':not(.is_3d)')), + status = release.status; + + if(!q && (status == 'snatched' || status == 'seeding' || status == 'done')) + q = self.addQuality(release.quality, release.is_3d || false); + + if (q && !q.hasClass(status)){ + q.addClass(status); + q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status) + } + + }); + }, + + addQuality: function(quality, is_3d){ + var self = this, + q = Quality.getQuality(quality); + + return new Element('span', { + 'text': q.label + (is_3d ? ' 3D' : ''), + 'class': 'q_'+q.identifier + (is_3d ? ' is_3d' : ''), + 'title': '' + }).inject(self.quality); + }, + + getTitle: function(){ + var self = this; + + var title = ''; + + if(self.data.info.number) { + title = 'Season ' + self.data.info.number; + } else { + // Season 0 / Specials + title = 'Specials'; + } + + return title; + }, + + getIdentifier: function(){ + var self = this; + + try { + return self.get('identifiers').imdb; + } + catch (e){ } + + return self.get('imdb'); + }, + + get: function(attr){ + return this.data[attr] || this.data.info[attr] + } +}); \ No newline at end of file diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index 0b223b7..6430bbe 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -734,6 +734,31 @@ bottom: auto; } + .shows .list .episodes .item.season .data { + padding-top: 4px; + padding-bottom: 4px; + height: auto; + + font-weight: bold; + font-size: 14px; + } + + .shows .list .episodes .item.season .data span.name { + width: 400px; + } + + .shows .list .episodes .item.season .data span.quality { + opacity: 0; + } + + .shows .list .episodes .item.season:hover .data span.quality { + opacity: 0.6; + } + + .shows .list .episodes .item.season .data span.quality:hover { + opacity: 1; + } + .shows .list .episodes .episode-options { display: block; @@ -765,16 +790,44 @@ width: 85px; } - .shows .list .episodes .episode-actions { + .shows .list .episodes .item-actions { position: absolute; width: auto; right: 0; - top: 0; + + display: none; + opacity: 0; border-left: none; } - .shows .list .show .episodes .episode-actions .refresh { + .shows .list .episodes .item:hover .item-actions { + display: inline-block; + opacity: 1; + } + + .shows .list .episodes .item:hover .item-action { + opacity: 0.6; + } + + .shows .list .episodes .item .item-action { + display: inline-block; + height: 22px; + min-width: 33px; + + line-height: 26px; + text-align: center; + font-size: 13px; + + margin-left: 1px; + padding: 0 5px; + } + + .shows .list .episodes .item .item-action:hover { + opacity: 1; + } + + .shows .list .show .episodes .refresh { color: #cbeecc; } diff --git a/couchpotato/core/media/show/_base/static/show.episodes.js b/couchpotato/core/media/show/_base/static/show.episodes.js index 5fa6645..c622e74 100755 --- a/couchpotato/core/media/show/_base/static/show.episodes.js +++ b/couchpotato/core/media/show/_base/static/show.episodes.js @@ -47,21 +47,9 @@ var Episodes = new Class({ createSeason: function(season) { var self = this, - title = ''; + s = new Season(self.show, self.options, season); - if(season.info.number) { - title = 'Season ' + season.info.number; - } else { - // Season 0 / Specials - title = 'Specials'; - } - - season['el'] = new Element('div', { - 'class': 'item head', - 'id': 'season_'+season._id - }).adopt( - new Element('span.name', {'text': title}) - ).inject(self.episodes_container); + $(s).inject(self.episodes_container); }, createEpisode: function(episode){ From efe0a4af53d46553d9bf53c9ca3e1f863edc7e94 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 22:03:54 +1200 Subject: [PATCH 42/59] [TV] Minor adjustments to season item UI --- couchpotato/core/media/show/_base/static/show.css | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/show.css b/couchpotato/core/media/show/_base/static/show.css index 6430bbe..ec20240 100755 --- a/couchpotato/core/media/show/_base/static/show.css +++ b/couchpotato/core/media/show/_base/static/show.css @@ -616,7 +616,7 @@ .shows .options .table .item:nth-child(even) { background: rgba(255,255,255,0.05); } - .shows .options .table .item:not(.head):hover { + .shows .options .table .item:not(.head):not(.data):hover { background: rgba(255,255,255,0.03); } @@ -717,6 +717,10 @@ transition: all .6s cubic-bezier(0.9,0,0.1,1); } + .shows .list .episodes .item.data { + background: none; + } + .shows .list .episodes .item.data span.episode { width: 40px; padding: 0 10px; @@ -734,6 +738,10 @@ bottom: auto; } + .shows .list .episodes .item.season:hover { + background: none !important; + } + .shows .list .episodes .item.season .data { padding-top: 4px; padding-bottom: 4px; @@ -748,14 +756,10 @@ } .shows .list .episodes .item.season .data span.quality { - opacity: 0; + opacity: 0.6; } .shows .list .episodes .item.season:hover .data span.quality { - opacity: 0.6; - } - - .shows .list .episodes .item.season .data span.quality:hover { opacity: 1; } From 74c7cf43812d15af36da3a2d0390cf3b3a6a8520 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 22:04:49 +1200 Subject: [PATCH 43/59] Added children to "library.related" --- couchpotato/core/media/_base/library/main.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/couchpotato/core/media/_base/library/main.py b/couchpotato/core/media/_base/library/main.py index da526ec..9e614fb 100755 --- a/couchpotato/core/media/_base/library/main.py +++ b/couchpotato/core/media/_base/library/main.py @@ -55,7 +55,7 @@ class Library(LibraryBase): ) def related(self, media): - result = {media['type']: media} + result = {self.key(media['type']): media} db = get_db() cur = media @@ -63,9 +63,17 @@ class Library(LibraryBase): while cur and cur.get('parent_id'): cur = db.get('id', cur['parent_id']) - parts = cur['type'].split('.') + result[self.key(cur['type'])] = cur - result[parts[-1]] = cur + children = db.get_many('media_children', media['_id'], with_doc = True) + + for item in children: + key = self.key(item['doc']['type']) + 's' + + if key not in result: + result[key] = [] + + result[key].append(item['doc']) return result @@ -94,8 +102,7 @@ class Library(LibraryBase): # Build children arrays for item in items: - parts = item['doc']['type'].split('.') - key = parts[-1] + 's' + key = self.key(item['doc']['type']) + 's' if key not in result: result[key] = {} @@ -115,3 +122,7 @@ class Library(LibraryBase): result['releases'] = fireEvent('release.for_media', result['_id'], single = True) return result + + def key(self, media_type): + parts = media_type.split('.') + return parts[-1] From 34bb8c79934ded3542c46fe1ffb47875dffd3790 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 22:05:56 +1200 Subject: [PATCH 44/59] [TV] Fixed issue retrieving episodes in season searcher --- couchpotato/core/media/show/searcher/season.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py index 6c1b5a4..1491acc 100755 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -57,7 +57,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): quality_order = fireEvent('quality.order', single = True) # Find 'active' episodes - episodes = media.get('episodes', []) + episodes = related['episodes'] episodes_active = [] for episode in episodes: From 68bde6086d5821bc0311013c63b1cf74eede19ff Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 22:56:59 +1200 Subject: [PATCH 45/59] [TV] Fixed incorrect 'release.delete' call in searcher and issue adding shows --- couchpotato/core/media/show/_base/main.py | 6 ++---- couchpotato/core/media/show/searcher/episode.py | 2 +- couchpotato/core/media/show/searcher/season.py | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 29af63a..41c7873 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -4,11 +4,9 @@ import traceback from couchpotato import get_db from couchpotato.api import addApiView from couchpotato.core.event import fireEvent, fireEventAsync, addEvent -from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.variable import getTitle, find from couchpotato.core.logger import CPLog from couchpotato.core.media import MediaBase -from qcond import QueryCondenser log = CPLog(__name__) @@ -110,7 +108,7 @@ class ShowBase(MediaBase): new = False try: - m = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, single = True)['doc'] + m = db.get('media', 'thetvdb-%s' % params.get('identifiers', {}).get('thetvdb'), with_doc = True)['doc'] except: new = True m = db.insert(media) @@ -155,7 +153,7 @@ class ShowBase(MediaBase): # Trigger update info if added and update_after: # Do full update to get images etc - fireEventAsync('show.update_extras', m, info, store = True, on_complete = onComplete) + fireEventAsync('show.update_extras', m.copy(), info, store = True, on_complete = onComplete) # Remove releases for rel in fireEvent('release.for_media', m['_id'], single = True): diff --git a/couchpotato/core/media/show/searcher/episode.py b/couchpotato/core/media/show/searcher/episode.py index 7e92537..ea3a9db 100755 --- a/couchpotato/core/media/show/searcher/episode.py +++ b/couchpotato/core/media/show/searcher/episode.py @@ -117,7 +117,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase): # Remove releases that aren't found anymore for release in releases: if release.get('status') == 'available' and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('id'), single = True) + fireEvent('release.delete', release.get('_id'), single = True) else: log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query)) fireEvent('media.restatus', media['_id']) diff --git a/couchpotato/core/media/show/searcher/season.py b/couchpotato/core/media/show/searcher/season.py index 1491acc..c51d584 100755 --- a/couchpotato/core/media/show/searcher/season.py +++ b/couchpotato/core/media/show/searcher/season.py @@ -135,7 +135,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase): # Remove releases that aren't found anymore for release in releases: if release.get('status') == 'available' and release.get('identifier') not in found_releases: - fireEvent('release.delete', release.get('id'), single = True) + fireEvent('release.delete', release.get('_id'), single = True) else: log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query)) fireEvent('media.restatus', media['_id']) From dea5bbbf1c0808d8a8037e1b380abfe4359831fe Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 3 Aug 2014 22:58:01 +1200 Subject: [PATCH 46/59] Update score plugin to use the "root" media (show, movie) title --- couchpotato/core/plugins/score/main.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) mode change 100644 => 100755 couchpotato/core/plugins/score/main.py diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py old mode 100644 new mode 100755 index 08a1855..f0e088c --- a/couchpotato/core/plugins/score/main.py +++ b/couchpotato/core/plugins/score/main.py @@ -1,4 +1,4 @@ -from couchpotato.core.event import addEvent +from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.variable import getTitle, splitString, removeDuplicate from couchpotato.core.logger import CPLog @@ -16,17 +16,20 @@ class Score(Plugin): def __init__(self): addEvent('score.calculate', self.calculate) - def calculate(self, nzb, movie): + def calculate(self, nzb, media): """ Calculate the score of a NZB, used for sorting later """ + # Fetch root media item (movie, show) + root = fireEvent('library.root', media, single = True) + # Merge global and category preferred_words = splitString(Env.setting('preferred_words', section = 'searcher').lower()) - try: preferred_words = removeDuplicate(preferred_words + splitString(movie['category']['preferred'].lower())) + try: preferred_words = removeDuplicate(preferred_words + splitString(media['category']['preferred'].lower())) except: pass - score = nameScore(toUnicode(nzb['name']), movie['info'].get('year'), preferred_words) + score = nameScore(toUnicode(nzb['name']), root['info'].get('year'), preferred_words) - for movie_title in movie['info']['titles']: + for movie_title in root['info']['titles']: score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title)) score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title)) @@ -44,15 +47,15 @@ class Score(Plugin): score += providerScore(nzb['provider']) # Duplicates in name - score += duplicateScore(nzb['name'], getTitle(movie)) + score += duplicateScore(nzb['name'], getTitle(root)) # Merge global and category ignored_words = splitString(Env.setting('ignored_words', section = 'searcher').lower()) - try: ignored_words = removeDuplicate(ignored_words + splitString(movie['category']['ignored'].lower())) + try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower())) except: pass # Partial ignored words - score += partialIgnoredScore(nzb['name'], getTitle(movie), ignored_words) + score += partialIgnoredScore(nzb['name'], getTitle(root), ignored_words) # Ignore single downloads from multipart score += halfMultipartScore(nzb['name']) From 7fbd89a3171c2d5a6b0e98ff8994eb194fe21f24 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Tue, 5 Aug 2014 16:14:18 +1200 Subject: [PATCH 47/59] [TV] Use trakt.tv for show searches (better text searching, posters) --- .../core/media/show/providers/info/thetvdb.py | 58 +-------------- .../core/media/show/providers/info/trakt.py | 86 ++++++++++++++++++++++ 2 files changed, 88 insertions(+), 56 deletions(-) create mode 100755 couchpotato/core/media/show/providers/info/trakt.py diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py index 4aa989c..e1d749f 100755 --- a/couchpotato/core/media/show/providers/info/thetvdb.py +++ b/couchpotato/core/media/show/providers/info/thetvdb.py @@ -12,7 +12,6 @@ from couchpotato.core.media.show.providers.base import ShowProvider from tvdb_api import tvdb_exceptions from tvdb_api.tvdb_api import Tvdb, Show - log = CPLog(__name__) autoload = 'TheTVDb' @@ -26,8 +25,6 @@ class TheTVDb(ShowProvider): # TODO: Expose apikey in setting so it can be changed by user def __init__(self): - addEvent('info.search', self.search, priority = 1) - addEvent('show.search', self.search, priority = 1) addEvent('show.info', self.getShowInfo, priority = 1) addEvent('season.info', self.getSeasonInfo, priority = 1) addEvent('episode.info', self.getEpisodeInfo, priority = 1) @@ -44,57 +41,6 @@ class TheTVDb(ShowProvider): self.tvdb = Tvdb(**self.tvdb_api_parms) self.valid_languages = self.tvdb.config['valid_languages'] - def search(self, q, limit = 12, language = 'en'): - ''' Find show by name - show = { 'id': 74713, - 'language': 'en', - 'lid': 7, - 'seriesid': '74713', - 'seriesname': u'Breaking Bad',} - ''' - - if self.isDisabled(): - return False - - if language != self.tvdb_api_parms['language'] and language in self.valid_languages: - self.tvdb_api_parms['language'] = language - self._setup() - - query = q - #query = simplifyString(query) - cache_key = 'thetvdb.cache.search.%s.%s' % (query, limit) - results = self.getCache(cache_key) - - if not results: - log.debug('Searching for show: %s', q) - - raw = None - try: - raw = self.tvdb.search(query) - except (tvdb_exceptions.tvdb_error, IOError), e: - log.error('Failed searching TheTVDB for "%s": %s', (query, traceback.format_exc())) - return False - - results = [] - if raw: - try: - nr = 0 - for show_info in raw: - - 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', (q, traceback.format_exc())) - return False - - return results - def getShow(self, identifier = None): show = None try: @@ -400,9 +346,9 @@ class TheTVDb(ShowProvider): def isDisabled(self): if self.conf('api_key') == '': log.error('No API key provided.') - True + return True else: - False + return False config = [{ diff --git a/couchpotato/core/media/show/providers/info/trakt.py b/couchpotato/core/media/show/providers/info/trakt.py new file mode 100755 index 0000000..cac37c1 --- /dev/null +++ b/couchpotato/core/media/show/providers/info/trakt.py @@ -0,0 +1,86 @@ +import urllib + +from couchpotato.core.event import addEvent +from couchpotato.core.logger import CPLog +from couchpotato.core.media.show.providers.base import ShowProvider + +log = CPLog(__name__) + +autoload = 'Trakt' + + +class Trakt(ShowProvider): + api_key = 'c043de5ada9d180028c10229d2a3ea5b' + base_url = 'http://api.trakt.tv/%%s.json/%s' % api_key + + def __init__(self): + addEvent('info.search', self.search, priority = 1) + addEvent('show.search', self.search, priority = 1) + + def search(self, q, limit = 12): + if self.isDisabled(): + return False + + # Check for cached result + cache_key = 'trakt.cache.search.%s.%s' % (q, limit) + results = self.getCache(cache_key) or [] + + if results: + return results + + # Search + log.debug('Searching for show: "%s"', q) + response = self._request('search/shows', query=q, limit=limit) + + if not response: + return [] + + # Parse search results + for show in response: + results.append(self._parseShow(show)) + + log.info('Found: %s', [result['titles'][0] + ' (' + str(result.get('year', 0)) + ')' for result in results]) + + self.setCache(cache_key, results) + return results + + def _request(self, action, **kwargs): + url = self.base_url % action + + if kwargs: + url += '?' + urllib.urlencode(kwargs) + + return self.getJsonData(url) + + def _parseShow(self, show): + # Images + images = show.get('images', {}) + + poster = images.get('poster') + backdrop = images.get('backdrop') + + # Rating + rating = show.get('ratings', {}).get('percentage') + + # Build show dict + show_data = { + 'identifiers': { + 'thetvdb': show.get('tvdb_id'), + 'imdb': show.get('imdb_id'), + 'tvrage': show.get('tvrage_id'), + }, + 'type': 'show', + 'titles': [show.get('title')], + 'images': { + 'poster': [poster] if poster else [], + 'backdrop': [backdrop] if backdrop else [], + 'poster_original': [], + 'backdrop_original': [], + }, + 'year': show.get('year'), + 'rating': { + 'trakt': float(rating) / 10 + }, + } + + return dict((k, v) for k, v in show_data.iteritems() if v) From 7f466f9c08862ddbee9995e24153ae69d2e3ba68 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 7 Aug 2014 12:45:58 +1200 Subject: [PATCH 48/59] [TV] Split matcher into separate modules --- couchpotato/core/media/show/matcher.py | 131 ------------------------ couchpotato/core/media/show/matcher/__init__.py | 7 ++ couchpotato/core/media/show/matcher/base.py | 72 +++++++++++++ couchpotato/core/media/show/matcher/episode.py | 30 ++++++ couchpotato/core/media/show/matcher/main.py | 9 ++ couchpotato/core/media/show/matcher/season.py | 27 +++++ 6 files changed, 145 insertions(+), 131 deletions(-) delete mode 100755 couchpotato/core/media/show/matcher.py create mode 100755 couchpotato/core/media/show/matcher/__init__.py create mode 100755 couchpotato/core/media/show/matcher/base.py create mode 100755 couchpotato/core/media/show/matcher/episode.py create mode 100755 couchpotato/core/media/show/matcher/main.py create mode 100755 couchpotato/core/media/show/matcher/season.py diff --git a/couchpotato/core/media/show/matcher.py b/couchpotato/core/media/show/matcher.py deleted file mode 100755 index 4137c1c..0000000 --- a/couchpotato/core/media/show/matcher.py +++ /dev/null @@ -1,131 +0,0 @@ -from couchpotato import CPLog -from couchpotato.core.event import addEvent, fireEvent -from couchpotato.core.helpers.variable import tryInt -from couchpotato.core.media._base.matcher.base import MatcherBase -from couchpotato.core.media._base.providers.base import MultiProvider - -log = CPLog(__name__) - -autoload = 'ShowMatcher' - - -class ShowMatcher(MultiProvider): - - def getTypes(self): - return [Season, Episode] - - -class Base(MatcherBase): - - # TODO come back to this later, think this could be handled better, this is starting to get out of hand.... - quality_map = { - 'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']}, - 'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']}, - - 'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']}, - 'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']}, - - 'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']}, - 'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']}, - - 'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]}, - 'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]}, - 'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]}, - - 'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']}, - 'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']}, - } - - def __init__(self): - super(Base, self).__init__() - - addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier) - - def correct(self, chain, release, media, quality): - log.info("Checking if '%s' is valid", release['name']) - log.info2('Release parsed as: %s', chain.info) - - if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True): - log.info('Wrong: %s, quality does not match', release['name']) - return False - - if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media): - log.info('Wrong: %s, identifier does not match', release['name']) - return False - - if not fireEvent('matcher.correct_title', chain, media): - log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name']))) - return False - - return True - - def correctIdentifier(self, chain, media): - raise NotImplementedError() - - def getChainIdentifier(self, chain): - if 'identifier' not in chain.info: - return None - - identifier = self.flattenInfo(chain.info['identifier']) - - # Try cast values to integers - for key, value in identifier.items(): - if isinstance(value, list): - if len(value) <= 1: - value = value[0] - else: - log.warning('Wrong: identifier contains multiple season or episode values, unsupported') - return None - - identifier[key] = tryInt(value, value) - - return identifier - - -class Episode(Base): - type = 'show.episode' - - def correctIdentifier(self, chain, media): - identifier = self.getChainIdentifier(chain) - if not identifier: - log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)') - return False - - # TODO - Parse episode ranges from identifier to determine if they are multi-part episodes - if any([x in identifier for x in ['episode_from', 'episode_to']]): - log.info2('Wrong: releases with identifier ranges are not supported yet') - return False - - required = fireEvent('library.identifier', media, single = True) - - # TODO - Support air by date episodes - # TODO - Support episode parts - - if identifier != required: - log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) - return False - - return True - - -class Season(Base): - type = 'show.season' - - def correctIdentifier(self, chain, media): - identifier = self.getChainIdentifier(chain) - if not identifier: - log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)') - return False - - # TODO - Parse episode ranges from identifier to determine if they are season packs - if any([x in identifier for x in ['episode_from', 'episode_to']]): - log.info2('Wrong: releases with identifier ranges are not supported yet') - return False - - required = fireEvent('library.identifier', media, single = True) - - if identifier != required: - log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) - return False - - return True diff --git a/couchpotato/core/media/show/matcher/__init__.py b/couchpotato/core/media/show/matcher/__init__.py new file mode 100755 index 0000000..e2e607a --- /dev/null +++ b/couchpotato/core/media/show/matcher/__init__.py @@ -0,0 +1,7 @@ +from .main import ShowMatcher + + +def autoload(): + return ShowMatcher() + +config = [] diff --git a/couchpotato/core/media/show/matcher/base.py b/couchpotato/core/media/show/matcher/base.py new file mode 100755 index 0000000..186334f --- /dev/null +++ b/couchpotato/core/media/show/matcher/base.py @@ -0,0 +1,72 @@ +from couchpotato import fireEvent, CPLog, tryInt +from couchpotato.core.event import addEvent +from couchpotato.core.media._base.matcher.base import MatcherBase + +log = CPLog(__name__) + + +class Base(MatcherBase): + + # TODO come back to this later, think this could be handled better, this is starting to get out of hand.... + quality_map = { + 'bluray_1080p': {'resolution': ['1080p'], 'source': ['bluray']}, + 'bluray_720p': {'resolution': ['720p'], 'source': ['bluray']}, + + 'bdrip_1080p': {'resolution': ['1080p'], 'source': ['BDRip']}, + 'bdrip_720p': {'resolution': ['720p'], 'source': ['BDRip']}, + + 'brrip_1080p': {'resolution': ['1080p'], 'source': ['BRRip']}, + 'brrip_720p': {'resolution': ['720p'], 'source': ['BRRip']}, + + 'webdl_1080p': {'resolution': ['1080p'], 'source': ['webdl', ['web', 'dl']]}, + 'webdl_720p': {'resolution': ['720p'], 'source': ['webdl', ['web', 'dl']]}, + 'webdl_480p': {'resolution': ['480p'], 'source': ['webdl', ['web', 'dl']]}, + + 'hdtv_720p': {'resolution': ['720p'], 'source': ['hdtv']}, + 'hdtv_sd': {'resolution': ['480p', None], 'source': ['hdtv']}, + } + + def __init__(self): + super(Base, self).__init__() + + addEvent('%s.matcher.correct_identifier' % self.type, self.correctIdentifier) + + def correct(self, chain, release, media, quality): + log.info("Checking if '%s' is valid", release['name']) + log.info2('Release parsed as: %s', chain.info) + + if not fireEvent('matcher.correct_quality', chain, quality, self.quality_map, single = True): + log.info('Wrong: %s, quality does not match', release['name']) + return False + + if not fireEvent('%s.matcher.correct_identifier' % self.type, chain, media): + log.info('Wrong: %s, identifier does not match', release['name']) + return False + + if not fireEvent('matcher.correct_title', chain, media): + log.info("Wrong: '%s', undetermined naming.", (' '.join(chain.info['show_name']))) + return False + + return True + + def correctIdentifier(self, chain, media): + raise NotImplementedError() + + def getChainIdentifier(self, chain): + if 'identifier' not in chain.info: + return None + + identifier = self.flattenInfo(chain.info['identifier']) + + # Try cast values to integers + for key, value in identifier.items(): + if isinstance(value, list): + if len(value) <= 1: + value = value[0] + else: + log.warning('Wrong: identifier contains multiple season or episode values, unsupported') + return None + + identifier[key] = tryInt(value, value) + + return identifier diff --git a/couchpotato/core/media/show/matcher/episode.py b/couchpotato/core/media/show/matcher/episode.py new file mode 100755 index 0000000..fb8a37e --- /dev/null +++ b/couchpotato/core/media/show/matcher/episode.py @@ -0,0 +1,30 @@ +from couchpotato import fireEvent, CPLog +from couchpotato.core.media.show.matcher.base import Base + +log = CPLog(__name__) + + +class Episode(Base): + type = 'show.episode' + + def correctIdentifier(self, chain, media): + identifier = self.getChainIdentifier(chain) + if not identifier: + log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)') + return False + + # TODO - Parse episode ranges from identifier to determine if they are multi-part episodes + if any([x in identifier for x in ['episode_from', 'episode_to']]): + log.info2('Wrong: releases with identifier ranges are not supported yet') + return False + + required = fireEvent('library.identifier', media, single = True) + + # TODO - Support air by date episodes + # TODO - Support episode parts + + if identifier != required: + log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) + return False + + return True diff --git a/couchpotato/core/media/show/matcher/main.py b/couchpotato/core/media/show/matcher/main.py new file mode 100755 index 0000000..e9eee6c --- /dev/null +++ b/couchpotato/core/media/show/matcher/main.py @@ -0,0 +1,9 @@ +from couchpotato.core.media._base.providers.base import MultiProvider +from couchpotato.core.media.show.matcher.episode import Episode +from couchpotato.core.media.show.matcher.season import Season + + +class ShowMatcher(MultiProvider): + + def getTypes(self): + return [Season, Episode] diff --git a/couchpotato/core/media/show/matcher/season.py b/couchpotato/core/media/show/matcher/season.py new file mode 100755 index 0000000..2bc64ca --- /dev/null +++ b/couchpotato/core/media/show/matcher/season.py @@ -0,0 +1,27 @@ +from couchpotato import fireEvent, CPLog +from couchpotato.core.media.show.matcher.base import Base + +log = CPLog(__name__) + + +class Season(Base): + type = 'show.season' + + def correctIdentifier(self, chain, media): + identifier = self.getChainIdentifier(chain) + if not identifier: + log.info2('Wrong: release identifier is not valid (unsupported or missing identifier)') + return False + + # TODO - Parse episode ranges from identifier to determine if they are season packs + if any([x in identifier for x in ['episode_from', 'episode_to']]): + log.info2('Wrong: releases with identifier ranges are not supported yet') + return False + + required = fireEvent('library.identifier', media, single = True) + + if identifier != required: + log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) + return False + + return True From 5d886ccf1ff863b070ce45f5ea404f1903a702de Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 7 Aug 2014 12:46:39 +1200 Subject: [PATCH 49/59] [TV] Moved "episode" and "season" modules into "show/_base/", fixed episode update bug --- couchpotato/core/media/show/_base/episode.py | 109 +++++++++++++++++++++++++++ couchpotato/core/media/show/_base/season.py | 94 +++++++++++++++++++++++ couchpotato/core/media/show/episode.py | 107 -------------------------- couchpotato/core/media/show/season.py | 94 ----------------------- 4 files changed, 203 insertions(+), 201 deletions(-) create mode 100755 couchpotato/core/media/show/_base/episode.py create mode 100755 couchpotato/core/media/show/_base/season.py delete mode 100755 couchpotato/core/media/show/episode.py delete mode 100755 couchpotato/core/media/show/season.py diff --git a/couchpotato/core/media/show/_base/episode.py b/couchpotato/core/media/show/_base/episode.py new file mode 100755 index 0000000..400c8e7 --- /dev/null +++ b/couchpotato/core/media/show/_base/episode.py @@ -0,0 +1,109 @@ +from couchpotato import get_db +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.logger import CPLog +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.media import MediaBase + + +log = CPLog(__name__) + +autoload = 'Episode' + + +class Episode(MediaBase): + + def __init__(self): + addEvent('show.episode.add', self.add) + addEvent('show.episode.update', self.update) + addEvent('show.episode.update_extras', self.updateExtras) + + def add(self, parent_id, info = None, update_after = True, status = None): + if not info: info = {} + + identifiers = info.pop('identifiers', None) + + if not identifiers: + log.warning('Unable to add episode, missing identifiers (info provider mismatch?)') + return + + # Add Season + episode_info = { + '_t': 'media', + 'type': 'show.episode', + 'identifiers': identifiers, + 'status': status if status else 'active', + 'parent_id': parent_id, + 'info': info, # Returned dict by providers + } + + # Check if season already exists + existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) + + db = get_db() + + if existing_episode: + s = existing_episode['doc'] + s.update(episode_info) + + episode = db.update(s) + else: + episode = db.insert(episode_info) + + # Update library info + if update_after is not False: + handle = fireEventAsync if update_after is 'async' else fireEvent + handle('show.episode.update_extras', episode, info, store = True, single = True) + + return episode + + def update(self, media_id = None, identifiers = None, info = None): + if not info: info = {} + + if self.shuttingDown(): + return + + db = get_db() + + episode = db.get('id', media_id) + + # Get new info + if not info: + season = db.get('id', episode['parent_id']) + show = db.get('id', season['parent_id']) + + info = fireEvent( + 'episode.info', show.get('identifiers'), { + 'season_identifiers': season.get('identifiers'), + 'season_number': season.get('info', {}).get('number'), + + 'episode_identifiers': episode.get('identifiers'), + 'episode_number': episode.get('info', {}).get('number'), + + 'absolute_number': episode.get('info', {}).get('absolute_number') + }, + merge = True + ) + + info['season_number'] = season.get('info', {}).get('number') + + identifiers = info.pop('identifiers', None) or identifiers + + # Update/create media + episode['identifiers'].update(identifiers) + episode.update({'info': info}) + + self.updateExtras(episode, info) + + db.update(episode) + return episode + + def updateExtras(self, episode, info, store=False): + db = get_db() + + # Get images + image_urls = info.get('images', []) + existing_files = episode.get('files', {}) + self.getPoster(image_urls, existing_files) + + if store: + db.update(episode) diff --git a/couchpotato/core/media/show/_base/season.py b/couchpotato/core/media/show/_base/season.py new file mode 100755 index 0000000..e41e460 --- /dev/null +++ b/couchpotato/core/media/show/_base/season.py @@ -0,0 +1,94 @@ +from couchpotato import get_db +from couchpotato.core.event import addEvent, fireEvent, fireEventAsync +from couchpotato.core.logger import CPLog +from couchpotato.core.helpers.variable import tryInt +from couchpotato.core.media import MediaBase + + +log = CPLog(__name__) + +autoload = 'Season' + + +class Season(MediaBase): + + def __init__(self): + addEvent('show.season.add', self.add) + addEvent('show.season.update', self.update) + addEvent('show.season.update_extras', self.updateExtras) + + def add(self, parent_id, info = None, update_after = True, status = None): + if not info: info = {} + + identifiers = info.pop('identifiers', None) + info.pop('episodes', None) + + # Add Season + season_info = { + '_t': 'media', + 'type': 'show.season', + 'identifiers': identifiers, + 'status': status if status else 'active', + 'parent_id': parent_id, + 'info': info, # Returned dict by providers + } + + # Check if season already exists + existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) + + db = get_db() + + if existing_season: + s = existing_season['doc'] + s.update(season_info) + + season = db.update(s) + else: + season = db.insert(season_info) + + # Update library info + if update_after is not False: + handle = fireEventAsync if update_after is 'async' else fireEvent + handle('show.season.update_extras', season, info, store = True, single = True) + + return season + + def update(self, media_id = None, identifiers = None, info = None): + if not info: info = {} + + if self.shuttingDown(): + return + + db = get_db() + + season = db.get('id', media_id) + show = db.get('id', season['parent_id']) + + # Get new info + if not info: + info = fireEvent('season.info', show.get('identifiers'), { + 'season_number': season.get('info', {}).get('number', 0) + }, merge = True) + + identifiers = info.pop('identifiers', None) or identifiers + info.pop('episodes', None) + + # Update/create media + season['identifiers'].update(identifiers) + season.update({'info': info}) + + self.updateExtras(season, info) + + db.update(season) + return season + + def updateExtras(self, season, info, store=False): + db = get_db() + + # Get images + image_urls = info.get('images', []) + existing_files = season.get('files', {}) + self.getPoster(image_urls, existing_files) + + if store: + db.update(season) diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py deleted file mode 100755 index 0557bb4..0000000 --- a/couchpotato/core/media/show/episode.py +++ /dev/null @@ -1,107 +0,0 @@ -from couchpotato import get_db -from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.logger import CPLog -from couchpotato.core.helpers.variable import tryInt -from couchpotato.core.media import MediaBase - - -log = CPLog(__name__) - -autoload = 'Episode' - - -class Episode(MediaBase): - - def __init__(self): - addEvent('show.episode.add', self.add) - addEvent('show.episode.update', self.update) - addEvent('show.episode.update_extras', self.updateExtras) - - def add(self, parent_id, info = None, update_after = True, status = None): - if not info: info = {} - - identifiers = info.pop('identifiers', None) - - if not identifiers: - log.warning('Unable to add episode, missing identifiers (info provider mismatch?)') - return - - # Add Season - episode_info = { - '_t': 'media', - 'type': 'show.episode', - 'identifiers': identifiers, - 'status': status if status else 'active', - 'parent_id': parent_id, - 'info': info, # Returned dict by providers - } - - # Check if season already exists - existing_episode = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) - - db = get_db() - - if existing_episode: - s = existing_episode['doc'] - s.update(episode_info) - - episode = db.update(s) - else: - episode = db.insert(episode_info) - - # Update library info - if update_after is not False: - handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.episode.update_extras', episode, info, store = True, single = True) - - return episode - - def update(self, media_id = None, identifiers = None, info = None): - if not info: info = {} - - if self.shuttingDown(): - return - - db = get_db() - - episode = db.get('id', media_id) - - # Get new info - if not info: - season = db.get('id', episode['parent_id']) - show = db.get('id', season['parent_id']) - - info = fireEvent( - 'episode.info', show.get('identifiers'), { - 'season_identifiers': season.get('identifiers'), - 'season_number': season.get('info', {}).get('number'), - - 'episode_identifiers': episode.get('identifiers'), - 'episode_number': episode.get('info', {}).get('number'), - - 'absolute_number': episode.get('info', {}).get('absolute_number') - }, - merge = True - ) - - identifiers = info.pop('identifiers', None) or identifiers - - # Update/create media - episode['identifiers'].update(identifiers) - episode.update({'info': info}) - - self.updateExtras(episode, info) - - db.update(episode) - return episode - - def updateExtras(self, episode, info, store=False): - db = get_db() - - # Get images - image_urls = info.get('images', []) - existing_files = episode.get('files', {}) - self.getPoster(image_urls, existing_files) - - if store: - db.update(episode) diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py deleted file mode 100755 index e41e460..0000000 --- a/couchpotato/core/media/show/season.py +++ /dev/null @@ -1,94 +0,0 @@ -from couchpotato import get_db -from couchpotato.core.event import addEvent, fireEvent, fireEventAsync -from couchpotato.core.logger import CPLog -from couchpotato.core.helpers.variable import tryInt -from couchpotato.core.media import MediaBase - - -log = CPLog(__name__) - -autoload = 'Season' - - -class Season(MediaBase): - - def __init__(self): - addEvent('show.season.add', self.add) - addEvent('show.season.update', self.update) - addEvent('show.season.update_extras', self.updateExtras) - - def add(self, parent_id, info = None, update_after = True, status = None): - if not info: info = {} - - identifiers = info.pop('identifiers', None) - info.pop('episodes', None) - - # Add Season - season_info = { - '_t': 'media', - 'type': 'show.season', - 'identifiers': identifiers, - 'status': status if status else 'active', - 'parent_id': parent_id, - 'info': info, # Returned dict by providers - } - - # Check if season already exists - existing_season = fireEvent('media.with_identifiers', identifiers, with_doc = True, single = True) - - db = get_db() - - if existing_season: - s = existing_season['doc'] - s.update(season_info) - - season = db.update(s) - else: - season = db.insert(season_info) - - # Update library info - if update_after is not False: - handle = fireEventAsync if update_after is 'async' else fireEvent - handle('show.season.update_extras', season, info, store = True, single = True) - - return season - - def update(self, media_id = None, identifiers = None, info = None): - if not info: info = {} - - if self.shuttingDown(): - return - - db = get_db() - - season = db.get('id', media_id) - show = db.get('id', season['parent_id']) - - # Get new info - if not info: - info = fireEvent('season.info', show.get('identifiers'), { - 'season_number': season.get('info', {}).get('number', 0) - }, merge = True) - - identifiers = info.pop('identifiers', None) or identifiers - info.pop('episodes', None) - - # Update/create media - season['identifiers'].update(identifiers) - season.update({'info': info}) - - self.updateExtras(season, info) - - db.update(season) - return season - - def updateExtras(self, season, info, store=False): - db = get_db() - - # Get images - image_urls = info.get('images', []) - existing_files = season.get('files', {}) - self.getPoster(image_urls, existing_files) - - if store: - db.update(season) From 478dc0f24274122f9715618d8fdec89b8c451424 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 7 Aug 2014 14:05:05 +1200 Subject: [PATCH 50/59] Changed "media.with_identifiers" to remove "No media found with..." messages --- couchpotato/core/media/_base/media/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index bcb8402..7021f82 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -185,17 +185,15 @@ class MediaPlugin(MediaBase): yield ms def withIdentifiers(self, identifiers, with_doc = False): - db = get_db() for x in identifiers: try: - media = db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) - return media + return db.get('media', '%s-%s' % (x, identifiers[x]), with_doc = with_doc) except: pass - log.debug('No media found with identifiers: %s', identifiers) + return False def list(self, types = None, status = None, release_status = None, status_or = False, limit_offset = None, with_tags = None, starts_with = None, search = None): From 12dd9c6b14fd88e3192b00d42e81fb21945aacc5 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 7 Aug 2014 14:06:31 +1200 Subject: [PATCH 51/59] [TV] Updated ShowBase.create() to use "media.with_identifiers" --- couchpotato/core/media/show/_base/main.py | 45 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 41c7873..5c9a942 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -78,8 +78,6 @@ class ShowBase(MediaBase): log.error('Failed adding media: %s', traceback.format_exc()) def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None): - db = get_db() - # Set default title def_title = self.getDefaultTitle(info) @@ -106,29 +104,34 @@ class ShowBase(MediaBase): # Update media with info self.updateInfo(media, info) - new = False - try: - m = db.get('media', 'thetvdb-%s' % params.get('identifiers', {}).get('thetvdb'), with_doc = True)['doc'] - except: - new = True - m = db.insert(media) + existing_show = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True) + + db = get_db() + + if existing_show: + s = existing_show['doc'] + s.update(media) + + show = db.update(s) + else: + show = db.insert(media) # Update dict to be usable - m.update(media) + show.update(media) added = True do_search = False search_after = search_after and self.conf('search_on_add', section = 'showsearcher') onComplete = None - if new: + if existing_show: if search_after: - onComplete = self.createOnComplete(m['_id']) + onComplete = self.createOnComplete(show['_id']) search_after = False elif force_readd: # Clean snatched history - for release in fireEvent('release.for_media', m['_id'], single = True): + for release in fireEvent('release.for_media', show['_id'], single = True): if release.get('status') in ['downloaded', 'snatched', 'done']: if params.get('ignore_previous', False): release['status'] = 'ignored' @@ -136,35 +139,35 @@ class ShowBase(MediaBase): 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()) + show['profile_id'] = params.get('profile_id', default_profile.get('id')) + show['category_id'] = media.get('category_id') + show['last_edit'] = int(time.time()) do_search = True - db.update(m) + db.update(show) else: params.pop('info', None) log.debug('Show already exists, not updating: %s', params) added = False # Create episodes - self.createEpisodes(m, seasons) + self.createEpisodes(show, seasons) # Trigger update info if added and update_after: # Do full update to get images etc - fireEventAsync('show.update_extras', m.copy(), info, store = True, on_complete = onComplete) + fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete) # Remove releases - for rel in fireEvent('release.for_media', m['_id'], single = True): + for rel in fireEvent('release.for_media', show['_id'], single = True): if rel['status'] is 'available': db.delete(rel) if do_search and search_after: - onComplete = self.createOnComplete(m['_id']) + onComplete = self.createOnComplete(show['_id']) onComplete() - return m, added + return show, added def createEpisodes(self, m, seasons_info): # Add Seasons From 5e438e5343de23450aa08e50308f77ae69e29b38 Mon Sep 17 00:00:00 2001 From: seedzero Date: Tue, 29 Jul 2014 18:13:02 +1000 Subject: [PATCH 52/59] Stop movie searcher searching for TV shows and hosing episodes --- couchpotato/core/media/movie/searcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/movie/searcher.py b/couchpotato/core/media/movie/searcher.py index e943c21..a8d6fe5 100755 --- a/couchpotato/core/media/movie/searcher.py +++ b/couchpotato/core/media/movie/searcher.py @@ -74,7 +74,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase): self.in_progress = True fireEvent('notify.frontend', type = 'movie.searcher.started', data = True, message = 'Full search started') - medias = [x['_id'] for x in fireEvent('media.with_status', 'active', with_doc = False, single = True)] + medias = [x['_id'] for x in fireEvent('media.with_status', 'active', 'movie', single = True)] random.shuffle(medias) total = len(medias) From ce80ac5a336435020d7962f0a0be546b4c565b9e Mon Sep 17 00:00:00 2001 From: seedzero Date: Wed, 6 Aug 2014 12:50:11 +1000 Subject: [PATCH 53/59] Fix show search not including quality profile --- couchpotato/core/media/show/_base/static/search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/media/show/_base/static/search.js b/couchpotato/core/media/show/_base/static/search.js index 91e2ed3..44bb894 100644 --- a/couchpotato/core/media/show/_base/static/search.js +++ b/couchpotato/core/media/show/_base/static/search.js @@ -185,7 +185,7 @@ Block.Search.ShowItem = new Class({ self.category_select.show(); categories.each(function(category){ new Element('option', { - 'value': category.data.id, + 'value': category.data._id, 'text': category.data.label }).inject(self.category_select); }); From c8f0cdc90f8d5319954121ee036a53e4e36d7c12 Mon Sep 17 00:00:00 2001 From: seedzero Date: Wed, 6 Aug 2014 14:30:59 +1000 Subject: [PATCH 54/59] Newznab search fixes --- .../core/media/show/providers/nzb/newznab.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/couchpotato/core/media/show/providers/nzb/newznab.py b/couchpotato/core/media/show/providers/nzb/newznab.py index e376d05..4354611 100644 --- a/couchpotato/core/media/show/providers/nzb/newznab.py +++ b/couchpotato/core/media/show/providers/nzb/newznab.py @@ -18,15 +18,15 @@ class Newznab(MultiProvider): class Season(SeasonProvider, Base): - def buildUrl(self, media, api_key): - search_title = fireEvent('media.search_query', media, include_identifier = False, single = True) - identifier = fireEvent('media.identifier', media, single = True) + def buildUrl(self, media, host): + related = fireEvent('library.related', media, single = True) + identifier = fireEvent('library.identifier', media, single = True) query = tryUrlencode({ 't': 'tvsearch', - 'q': search_title, + 'apikey': host['api_key'], + 'q': related['show']['title'], 'season': identifier['season'], - 'apikey': api_key, 'extended': 1 }) return query @@ -34,16 +34,15 @@ class Season(SeasonProvider, Base): class Episode(EpisodeProvider, Base): - def buildUrl(self, media, api_key): - search_title = fireEvent('media.search_query', media['show'], include_identifier = False, single = True) - identifier = fireEvent('media.identifier', media, single = True) - + def buildUrl(self, media, host): + related = fireEvent('library.related', media, single = True) + identifier = fireEvent('library.identifier', media, single = True) query = tryUrlencode({ 't': 'tvsearch', - 'q': search_title, + 'apikey': host['api_key'], + 'q': related['show']['title'], 'season': identifier['season'], 'ep': identifier['episode'], - 'apikey': api_key, 'extended': 1 }) From deb79432038d7a42ef9858d6a6512fe7526576a1 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sun, 10 Aug 2014 13:12:52 +1200 Subject: [PATCH 55/59] Fixed broken quality profile identifiers --- couchpotato/core/media/show/_base/static/search.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 couchpotato/core/media/show/_base/static/search.js diff --git a/couchpotato/core/media/show/_base/static/search.js b/couchpotato/core/media/show/_base/static/search.js old mode 100644 new mode 100755 index 44bb894..96498e5 --- a/couchpotato/core/media/show/_base/static/search.js +++ b/couchpotato/core/media/show/_base/static/search.js @@ -198,8 +198,8 @@ Block.Search.ShowItem = new Class({ profiles.each(function(profile){ new Element('option', { - 'value': profile.id ? profile.id : profile.data.id, - 'text': profile.label ? profile.label : profile.data.label + 'value': profile.get('_id'), + 'text': profile.get('label') }).inject(self.profile_select) }); From 2ad249b1956ad67b1eff7770a98de91dbc04197b Mon Sep 17 00:00:00 2001 From: seedzero Date: Mon, 18 Aug 2014 23:34:23 +1000 Subject: [PATCH 56/59] Fixed media.types & addSingleListView addSingleCharView, addSingleDeleteView --- couchpotato/core/media/_base/media/main.py | 9 +++------ couchpotato/core/media/show/_base/main.py | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 7021f82..8adc666 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -312,8 +312,7 @@ class MediaPlugin(MediaBase): def addSingleListView(self): for media_type in fireEvent('media.types', merge = True): - def tempList(*args, **kwargs): - return self.listView(types = media_type, **kwargs) + tempList = lambda media_type = media_type, *args, **kwargs : self.listView(type = media_type, **kwargs) addApiView('%s.list' % media_type, tempList) def availableChars(self, types = None, status = None, release_status = None): @@ -381,8 +380,7 @@ class MediaPlugin(MediaBase): def addSingleCharView(self): for media_type in fireEvent('media.types', merge = True): - def tempChar(*args, **kwargs): - return self.charView(types = media_type, **kwargs) + tempChar = lambda media_type = media_type, *args, **kwargs : self.charView(type = media_type, **kwargs) addApiView('%s.available_chars' % media_type, tempChar) def delete(self, media_id, delete_from = None): @@ -451,8 +449,7 @@ class MediaPlugin(MediaBase): def addSingleDeleteView(self): for media_type in fireEvent('media.types', merge = True): - def tempDelete(*args, **kwargs): - return self.deleteView(types = media_type, *args, **kwargs) + tempDelete = lambda media_type = media_type, *args, **kwargs : self.deleteView(type = media_type, **kwargs) addApiView('%s.delete' % media_type, tempDelete) def restatus(self, media_id): diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index 5c9a942..e27e489 100755 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -18,6 +18,7 @@ class ShowBase(MediaBase): def __init__(self): super(ShowBase, self).__init__() + self.initType() addApiView('show.add', self.addView, docs = { 'desc': 'Add new show to the wanted list', From ba14c95e824caf80305989124839f3285ff9ec32 Mon Sep 17 00:00:00 2001 From: seedzero Date: Tue, 19 Aug 2014 01:36:12 +1000 Subject: [PATCH 57/59] Documentation added for media type .list & .delete APIs --- couchpotato/core/media/_base/media/main.py | 36 +++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 8adc666..26140f3 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -44,15 +44,15 @@ class MediaPlugin(MediaBase): 'desc': 'List media', 'params': { 'type': {'type': 'string', 'desc': 'Media type to filter on.'}, - 'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'}, - 'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'}, - 'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'}, - 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'}, - 'search': {'desc': 'Search movie title'}, + 'status': {'type': 'array or csv', 'desc': 'Filter media by status. Example:"active,done"'}, + 'release_status': {'type': 'array or csv', 'desc': 'Filter media by status of its releases. Example:"snatched,available"'}, + 'limit_offset': {'desc': 'Limit and offset the media list. Examples: "50" or "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all media starting with the letter "a"'}, + 'search': {'desc': 'Search media title'}, }, 'return': {'type': 'object', 'example': """{ 'success': True, - 'empty': bool, any movies returned or not, + 'empty': bool, any media returned or not, 'media': array, media found, }"""} }) @@ -313,7 +313,21 @@ class MediaPlugin(MediaBase): for media_type in fireEvent('media.types', merge = True): tempList = lambda media_type = media_type, *args, **kwargs : self.listView(type = media_type, **kwargs) - addApiView('%s.list' % media_type, tempList) + addApiView('%s.list' % media_type, tempList, docs = { + 'desc': 'List media', + 'params': { + 'status': {'type': 'array or csv', 'desc': 'Filter ' + media_type + ' by status. Example:"active,done"'}, + 'release_status': {'type': 'array or csv', 'desc': 'Filter ' + media_type + ' by status of its releases. Example:"snatched,available"'}, + 'limit_offset': {'desc': 'Limit and offset the ' + media_type + ' list. Examples: "50" or "50,30"'}, + 'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all ' + media_type + 's starting with the letter "a"'}, + 'search': {'desc': 'Search ' + media_type + ' title'}, + }, + 'return': {'type': 'object', 'example': """{ + 'success': True, + 'empty': bool, any """ + media_type + """s returned or not, + 'media': array, media found, + }"""} + }) def availableChars(self, types = None, status = None, release_status = None): @@ -450,7 +464,13 @@ class MediaPlugin(MediaBase): for media_type in fireEvent('media.types', merge = True): tempDelete = lambda media_type = media_type, *args, **kwargs : self.deleteView(type = media_type, **kwargs) - addApiView('%s.delete' % media_type, tempDelete) + addApiView('%s.delete' % media_type, tempDelete, docs = { + 'desc': 'Delete a ' + media_type + ' from the wanted list', + 'params': { + 'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'}, + 'delete_from': {'desc': 'Delete ' + media_type + ' from this page', 'type': 'string: all (default), wanted, manage'}, + } + }) def restatus(self, media_id): From 02571d0f5d5922c932fca8a3090e3365f6c6d21b Mon Sep 17 00:00:00 2001 From: seedzero Date: Wed, 10 Sep 2014 23:28:55 +1000 Subject: [PATCH 58/59] 'list' API's, return as media type --- couchpotato/core/media/_base/media/main.py | 39 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py index 26140f3..b1424dc 100755 --- a/couchpotato/core/media/_base/media/main.py +++ b/couchpotato/core/media/_base/media/main.py @@ -258,10 +258,6 @@ class MediaPlugin(MediaBase): for x in filter_by: media_ids = [n for n in media_ids if n in filter_by[x]] - total_count = len(media_ids) - if total_count == 0: - return 0, [] - offset = 0 limit = -1 if limit_offset: @@ -287,11 +283,30 @@ class MediaPlugin(MediaBase): media_ids.remove(media_id) if len(media_ids) == 0 or len(medias) == limit: break - return total_count, medias + # Sort media by type and return result + result = {} + + # Create keys for media types we are listing + if types: + for media_type in types: + result['%ss' % media_type] = [] + else: + for media_type in fireEvent('media.types', merge = True): + result['%ss' % media_type] = [] + + total_count = len(medias) + + if total_count == 0: + return 0, result + + for kind in medias: + result['%ss' % kind['type']].append(kind) + + return total_count, result def listView(self, **kwargs): - total_movies, movies = self.list( + total_count, result = self.list( types = splitString(kwargs.get('type')), status = splitString(kwargs.get('status')), release_status = splitString(kwargs.get('release_status')), @@ -302,12 +317,12 @@ class MediaPlugin(MediaBase): search = kwargs.get('search') ) - return { - 'success': True, - 'empty': len(movies) == 0, - 'total': total_movies, - 'movies': movies, - } + results = result + results['success'] = True + results['empty'] = len(result) == 0 + results['total'] = total_count + + return results def addSingleListView(self): From bb609e073b082f5a07dce3f8b3daa1a1330a3405 Mon Sep 17 00:00:00 2001 From: seedzero Date: Thu, 11 Sep 2014 21:36:05 +1000 Subject: [PATCH 59/59] [TV] Fix for new 'list' API output --- couchpotato/core/media/show/_base/static/list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/media/show/_base/static/list.js b/couchpotato/core/media/show/_base/static/list.js index 87b085a..4177725 100755 --- a/couchpotato/core/media/show/_base/static/list.js +++ b/couchpotato/core/media/show/_base/static/list.js @@ -575,8 +575,8 @@ var ShowList = new Class({ self.el.setStyle('min-height', null); } - self.store(json.movies); - self.addMovies(json.movies, json.total || json.movies.length); + self.store(json.shows); + self.addMovies(json.shows, json.total || json.shows.length); if(self.scrollspy) { self.load_more.set('text', 'load more movies'); self.scrollspy.start();