diff --git a/couchpotato/core/media/movie/_base/main.py b/couchpotato/core/media/movie/_base/main.py index 448e9985..7e090fd6 100644 --- a/couchpotato/core/media/movie/_base/main.py +++ b/couchpotato/core/media/movie/_base/main.py @@ -346,7 +346,6 @@ class MovieBase(MovieTypeBase): } def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): - if not params.get('identifier'): msg = 'Can\'t add movie without imdb identifier.' log.error(msg) diff --git a/couchpotato/core/media/movie/_base/static/search.js b/couchpotato/core/media/movie/_base/static/search.js index 9663f9dc..e7fff409 100644 --- a/couchpotato/core/media/movie/_base/static/search.js +++ b/couchpotato/core/media/movie/_base/static/search.js @@ -11,7 +11,7 @@ Block.Search = new Class({ self.el = new Element('div.search_form').adopt( new Element('div.input').adopt( self.input = new Element('input', { - 'placeholder': 'Search & add a new show', + 'placeholder': 'Search & add a new movie', 'events': { 'keyup': self.keyup.bind(self), 'focus': function(){ @@ -66,7 +66,7 @@ Block.Search = new Class({ self.input.set('value', ''); self.input.focus() - self.shows = [] + self.movies = [] self.results.empty() self.el.removeClass('filled') @@ -131,7 +131,7 @@ Block.Search = new Class({ if(!self.spinner) self.spinner = createSpinner(self.mask); - self.api_request = Api.request('show.search', { + self.api_request = Api.request('movie.search', { 'data': { 'q': q }, @@ -150,17 +150,17 @@ Block.Search = new Class({ self.cache[q] = json - self.shows = {} + self.movies = {} self.results.empty() - Object.each(json.shows, function(show){ + Object.each(json.movies, function(movie){ - var m = new Block.Search.Item(show); + var m = new Block.Search.Item(movie); $(m).inject(self.results) - self.shows[show.imdb || 'r-'+Math.floor(Math.random()*10000)] = m + self.movies[movie.imdb || 'r-'+Math.floor(Math.random()*10000)] = m - if(q == show.imdb) - m.showOptions() + if(q == movie.imdb) + m.movieOptions() }); @@ -201,7 +201,7 @@ Block.Search.Item = new Class({ var self = this, info = self.info; - self.el = new Element('div.show_result', { + self.el = new Element('div.movie_result', { 'id': info.imdb }).adopt( self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { @@ -212,7 +212,7 @@ Block.Search.Item = new Class({ self.options_el = new Element('div.options.inlay'), self.data_container = new Element('div.data', { 'events': { - 'click': self.showOptions.bind(self) + 'click': self.movieOptions.bind(self) } }).adopt( self.info_container = new Element('div.info').adopt( @@ -256,7 +256,7 @@ Block.Search.Item = new Class({ return this.info[key] }, - showOptions: function(){ + movieOptions: function(){ var self = this; self.createOptions(); @@ -281,7 +281,7 @@ Block.Search.Item = new Class({ self.loadingMask(); - Api.request('show.add', { + Api.request('movie.add', { 'data': { 'identifier': self.info.imdb, 'title': self.title_select.get('value'), @@ -366,7 +366,7 @@ Block.Search.Item = new Class({ if(categories.length == 0) self.category_select.hide(); else { - self.category_select.show(); + self.category_select.movie(); categories.each(function(category){ new Element('option', { 'value': category.data.id, diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py index e9e29888..c04bbc0e 100644 --- a/couchpotato/core/media/show/_base/main.py +++ b/couchpotato/core/media/show/_base/main.py @@ -55,6 +55,16 @@ class ShowBase(MediaBase): 'shows': array, shows found, }"""} }) + addApiView('show.add', self.addView, docs = { + 'desc': 'Add new movie to the wanted list', + 'params': { + 'identifier': {'desc': 'IMDB id of the movie your want to add.'}, + 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, + 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, + } + }) + + addEvent('show.add', self.add) def search(self, q = '', **kwargs): @@ -75,3 +85,148 @@ class ShowBase(MediaBase): 'shows': shows, } + def addView(self, **kwargs): + + movie_dict = fireEvent('show.add', params=kwargs) # XXX: Temp added so we can catch a breakpoint + #movie_dict = self.add(params = kwargs) + + return { + 'success': True, + 'added': True if movie_dict else False, + 'movie': movie_dict, + } + + def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): + """ + 1. Add Show + 1. Add All Episodes + 2. Add All Seasons + + Notes, not to forget: + - relate parent and children, possible grandparent to grandchild so episodes know it belong to show, etc + - looks like we dont send info to library; it comes later + - change references to plot to description + - change Model to Media + """ + log.debug("show.add") + + + identifier = params.get('thetvdb_id') + episodes = fireEvent('show.episodes', identifier = identifier) + + # XXX: Fix so we dont have a nested list + for episode in episodes[0]: + self.add2(params=episode) + + return self.add2(params = params) + + def add2(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None): + log.debug("show.add2") + + if not params.get('identifier'): + msg = 'Can\'t add show without imdb identifier.' + log.error(msg) + fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg) + return False + #else: + #try: + #is_show = fireEvent('movie.is_show', identifier = params.get('identifier'), single = True) + #if not is_show: + #msg = 'Can\'t add show, seems to be a TV show.' + #log.error(msg) + #fireEvent('notify.frontend', type = 'show.is_tvshow', message = msg) + #return False + #except: + #pass + + + library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) + + # Status + status_active, snatched_status, ignored_status, done_status, downloaded_status = \ + fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True) + + default_profile = fireEvent('profile.default', single = True) + cat_id = params.get('category_id', None) + + db = get_session() + m = db.query(Movie).filter_by(library_id = library.get('id')).first() + added = True + do_search = False + if not m: + m = Movie( + library_id = library.get('id'), + profile_id = params.get('profile_id', default_profile.get('id')), + status_id = status_id if status_id else status_active.get('id'), + category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, + ) + db.add(m) + db.commit() + + onComplete = None + if search_after: + onComplete = self.createOnComplete(m.id) + + fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete) + search_after = False + elif force_readd: + + # Clean snatched history + for release in m.releases: + if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]: + if params.get('ignore_previous', False): + release.status_id = ignored_status.get('id') + else: + fireEvent('release.delete', release.id, single = True) + + m.profile_id = params.get('profile_id', default_profile.get('id')) + m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None + else: + log.debug('Movie already exists, not updating: %s', params) + added = False + + if force_readd: + m.status_id = status_id if status_id else status_active.get('id') + m.last_edit = int(time.time()) + do_search = True + + db.commit() + + # Remove releases + available_status = fireEvent('status.get', 'available', single = True) + for rel in m.releases: + if rel.status_id is available_status.get('id'): + db.delete(rel) + db.commit() + + show_dict = m.to_dict(self.default_dict) + + if do_search and search_after: + onComplete = self.createOnComplete(m.id) + onComplete() + + if added: + fireEvent('notify.frontend', type = 'show.added', data = show_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) + + db.expire_all() + return show_dict + + def createOnComplete(self, movie_id): + + def onComplete(): + db = get_session() + movie = db.query(Movie).filter_by(id = movie_id).first() + fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id)) + db.expire_all() + + return onComplete + + def createNotifyFront(self, movie_id): + + def notifyFront(): + db = get_session() + movie = db.query(Movie).filter_by(id = movie_id).first() + fireEvent('notify.frontend', type = 'show.update.%s' % movie.id, data = movie.to_dict(self.default_dict)) + db.expire_all() + + return notifyFront diff --git a/couchpotato/core/media/show/_base/static/search.css b/couchpotato/core/media/show/_base/static/search.css index 248a2a8e..ab648063 100644 --- a/couchpotato/core/media/show/_base/static/search.css +++ b/couchpotato/core/media/show/_base/static/search.css @@ -2,9 +2,9 @@ display: inline-block; vertical-align: middle; position: absolute; - right: 205px; + right: 135px; top: 0; - text-align: right; + text-align: left; height: 100%; border-bottom: 4px solid transparent; transition: all .4s cubic-bezier(0.9,0,0.1,1); diff --git a/couchpotato/core/media/show/_base/static/search.js b/couchpotato/core/media/show/_base/static/search.js index 015b2621..89530858 100644 --- a/couchpotato/core/media/show/_base/static/search.js +++ b/couchpotato/core/media/show/_base/static/search.js @@ -1,4 +1,4 @@ -Block.Search = new Class({ +Block.ShowSearch = new Class({ Extends: BlockBase, @@ -11,7 +11,7 @@ Block.Search = new Class({ self.el = new Element('div.show_search_form').adopt( new Element('div.input').adopt( self.input = new Element('input', { - 'placeholder': 'Search & add a new show, + 'placeholder': 'Search & add a new *show*', 'events': { 'keyup': self.keyup.bind(self), 'focus': function(){ @@ -155,7 +155,7 @@ Block.Search = new Class({ Object.each(json.shows, function(show){ - var m = new Block.Search.Item(show); + var m = new Block.ShowSearch.Item(show); $(m).inject(self.results) self.shows[show.imdb || 'r-'+Math.floor(Math.random()*10000)] = m @@ -183,7 +183,7 @@ Block.Search = new Class({ }); -Block.Search.Item = new Class({ +Block.ShowSearch.Item = new Class({ Implements: [Options, Events], @@ -284,6 +284,7 @@ Block.Search.Item = new Class({ Api.request('show.add', { 'data': { 'identifier': self.info.imdb, + 'thetvdb_id': self.info.thetvdb_id, 'title': self.title_select.get('value'), 'profile_id': self.profile_select.get('value'), 'category_id': self.category_select.get('value') diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py index 1842ed70..892497d8 100644 --- a/couchpotato/core/plugins/library/main.py +++ b/couchpotato/core/plugins/library/main.py @@ -20,7 +20,7 @@ class LibraryPlugin(Plugin): addEvent('library.update', self.update) addEvent('library.update_release_date', self.updateReleaseDate) - def add(self, attrs = {}, update_after = True): + def add(self, attrs = {}, update_after = True, type='movie'): db = get_session() @@ -28,6 +28,7 @@ class LibraryPlugin(Plugin): if not l: status = fireEvent('status.get', 'needs_update', single = True) l = Library( + type = type, year = attrs.get('year'), identifier = attrs.get('identifier'), plot = toUnicode(attrs.get('plot')), diff --git a/couchpotato/core/providers/show/thetvdb/main.py b/couchpotato/core/providers/show/thetvdb/main.py index f115414f..321fe36c 100644 --- a/couchpotato/core/providers/show/thetvdb/main.py +++ b/couchpotato/core/providers/show/thetvdb/main.py @@ -14,6 +14,7 @@ class TheTVDb(ShowProvider): #addEvent('show.by_hash', self.byHash) addEvent('show.search', self.search, priority = 1) addEvent('show.info', self.getInfo, priority = 1) + addEvent('show.episodes', self.getEpisodes, priority = 1) #addEvent('show.info_by_thetvdb', self.getInfoByTheTVDBId) # Use base wrapper @@ -100,11 +101,24 @@ class TheTVDb(ShowProvider): return results - def getInfo(self, identifier = None): - + def getEpisodes(self, identifier=None): if not identifier: - return {} - + return [] + + try: + show = self.tvdb[int(identifier)] + except: + return [] + + result = [] + for season in show.values(): + for episode in season.values(): + # Consider cache + result.append(self.parseEpisode(episode)) + + return result + + def getInfo(self, identifier = None): cache_key = 'thetvdb.cache.%s' % identifier result = self.getCache(cache_key) @@ -217,6 +231,85 @@ class TheTVDb(ShowProvider): #show_data['titles'].append(alt_name) return show_data + + def parseEpisode(self, episode): + """ + ('episodenumber', u'1'), + ('thumb_added', None), + ('rating', u'7.7'), + ('overview', + u'Experienced waitress Max Black meets her new co-worker, former rich-girl Caroline Channing, and puts her skills to the test at an old but re-emerging Brooklyn diner. Despite her initial distaste for Caroline, Max eventually softens and the two team up for a new business venture.'), + ('dvd_episodenumber', None), + ('dvd_discid', None), + ('combined_episodenumber', u'1'), + ('epimgflag', u'7'), + ('id', u'4099506'), + ('seasonid', u'465948'), + ('thumb_height', u'225'), + ('tms_export', u'1374789754'), + ('seasonnumber', u'1'), + ('writer', u'|Michael Patrick King|Whitney Cummings|'), + ('lastupdated', u'1371420338'), + ('filename', u'http://thetvdb.com/banners/episodes/248741/4099506.jpg'), + ('absolute_number', u'1'), + ('ratingcount', u'102'), + ('combined_season', u'1'), + ('thumb_width', u'400'), + ('imdb_id', u'tt1980319'), + ('director', u'James Burrows'), + ('dvd_chapter', None), + ('dvd_season', None), + ('gueststars', + u'|Brooke Lyons|Noah Mills|Shoshana Bush|Cale Hartmann|Adam Korson|Alex Enriquez|Matt Cook|Bill Parks|Eugene Shaw|Sergey Brusilovsky|Greg Lewis|Cocoa Brown|Nick Jameson|'), + ('seriesid', u'248741'), + ('language', u'en'), + ('productioncode', u'296793'), + ('firstaired', u'2011-09-19'), + ('episodename', u'Pilot')] + """ + + ## Images + #poster = self.getImage(episode, type = 'poster', size = 'cover') + #backdrop = self.getImage(episode, type = 'fanart', size = 'w1280') + ##poster_original = self.getImage(episode, type = 'poster', size = 'original') + ##backdrop_original = self.getImage(episode, type = 'backdrop', size = 'original') + poster = [] + backdrop = [] + + ## Genres + genres = [] + + ## Year (not really needed for episode) + year = None + + episode_data = { + 'via_thetvdb': True, + 'thetvdb_id': int(episode['id']), + 'titles': [episode['episodename'], ], + 'original_title': episode['episodename'], + 'images': { + 'poster': [poster] if poster else [], + 'backdrop': [backdrop] if backdrop else [], + 'poster_original': [], + 'backdrop_original': [], + }, + 'imdb': episode['imdb_id'], + 'runtime': None, + 'released': episode['firstaired'], + 'year': year, + 'plot': episode['overview'], + 'genres': genres, + } + + episode_data = dict((k, v) for k, v in episode_data.iteritems() if v) + + ## Add alternative names + #for alt in ['original_name', 'alternative_name']: + #alt_name = toUnicode(episode.get(alt)) + #if alt_name and not alt_name in episode_data['titles'] and alt_name.lower() != 'none' and alt_name != None: + #episode_data['titles'].append(alt_name) + + return episode_data def getImage(self, show, type = 'poster', size = 'cover'): """""" diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index f20c4203..57ce180a 100644 --- a/couchpotato/core/settings/model.py +++ b/couchpotato/core/settings/model.py @@ -91,6 +91,7 @@ class Library(Entity): """""" # For Movies, CPS uses three: omdbapi (no prio !?), tmdb (prio 2) and couchpotatoapi (prio 1) + type = Field(String(10), default="movie", index=True) provider = Field(String(10), default="imdb", index=True) year = Field(Integer) identifier = Field(String(20), index = True) diff --git a/couchpotato/static/scripts/couchpotato.js b/couchpotato/static/scripts/couchpotato.js index fdc9bd10..1c8d4607 100644 --- a/couchpotato/static/scripts/couchpotato.js +++ b/couchpotato/static/scripts/couchpotato.js @@ -71,6 +71,7 @@ new Element('div').adopt( self.block.navigation = new Block.Navigation(self, {}), self.block.search = new Block.Search(self, {}), + self.block.search = new Block.ShowSearch(self, {}), self.block.more = new Block.Menu(self, {'button_class': 'icon2.cog'}) ) ),