diff --git a/couchpotato/core/media/_base/media/main.py b/couchpotato/core/media/_base/media/main.py
index 01dc0f1..28221ba 100755
--- a/couchpotato/core/media/_base/media/main.py
+++ b/couchpotato/core/media/_base/media/main.py
@@ -273,10 +273,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:
@@ -306,11 +302,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')),
@@ -321,12 +336,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):
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/_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/main.py b/couchpotato/core/media/show/_base/main.py
old mode 100644
new mode 100755
index 7d7e637..e27e489
--- 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
+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__)
@@ -17,10 +15,10 @@ log = CPLog(__name__)
class ShowBase(MediaBase):
_type = 'show'
- query_condenser = QueryCondenser()
def __init__(self):
super(ShowBase, self).__init__()
+ self.initType()
addApiView('show.add', self.addView, docs = {
'desc': 'Add new show to the wanted list',
@@ -33,9 +31,8 @@ class ShowBase(MediaBase):
})
addEvent('show.add', self.add)
- addEvent('show.update_info', self.updateInfo)
-
- addEvent('media.search_query', self.query)
+ addEvent('show.update', self.update)
+ addEvent('show.update_extras', self.updateExtras)
def addView(self, **kwargs):
add_dict = self.add(params = kwargs)
@@ -48,8 +45,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.'
@@ -61,124 +56,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
- }
-
- # 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['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', {})
-
- season = fireEvent('show.season.add', m.get('_id'), season_info, 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)
+ m, added = self.create(info, params, force_readd, search_after, update_after)
+ 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:
@@ -187,17 +71,121 @@ 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())
- def updateInfo(self, media_id = None, identifiers = None, info = None):
- if not info: info = {}
- if not identifiers: identifiers = {}
+ def create(self, info, params = None, force_readd = True, search_after = True, update_after = True, notify_after = True, status = None):
+ # 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')
+
+ 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
+ }
+
+ identifiers = info.pop('identifiers', {})
+ seasons = info.pop('seasons', {})
+
+ # Update media with info
+ self.updateInfo(media, info)
+
+ 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
+ show.update(media)
+
+ added = True
+ do_search = False
+ search_after = search_after and self.conf('search_on_add', section = 'showsearcher')
+ onComplete = None
+
+ if existing_show:
+ if search_after:
+ onComplete = self.createOnComplete(show['_id'])
+
+ search_after = False
+ elif force_readd:
+ # Clean snatched history
+ 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'
+ db.update(release)
+ else:
+ fireEvent('release.delete', release['_id'], single = True)
+
+ 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(show)
+ else:
+ params.pop('info', None)
+ log.debug('Show already exists, not updating: %s', params)
+ added = False
+
+ # Create episodes
+ self.createEpisodes(show, seasons)
+
+ # Trigger update info
+ if added and update_after:
+ # Do full update to get images etc
+ fireEventAsync('show.update_extras', show.copy(), info, store = True, on_complete = onComplete)
+
+ # Remove releases
+ 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(show['_id'])
+ onComplete()
+
+ return show, 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_id = None, media = None, identifiers = None, info = None):
"""
Update movie information inside media['doc']['info']
@@ -212,68 +200,92 @@ class ShowBase(MediaBase):
@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:
- db = get_db()
-
- 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
- try: del info['identifiers']
- except: pass
- try: del info['in_wanted']
- except: pass
- try: del info['in_library']
- except: pass
-
- if not info or len(info) == 0:
- log.error('Could not update, no show info to work with: %s', media.get('identifier'))
- return False
-
- # Update basic info
- media['info'] = info
-
- # Update image file
- image_urls = info.get('images', [])
- existing_files = media.get('files', {})
- self.getPoster(image_urls, existing_files)
+ identifiers = info.pop('identifiers', {})
+ seasons = info.pop('seasons', {})
- db.update(media)
+ 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 query(self, media, first = True, condense = True, **kwargs):
- if media.get('type') != 'show':
- return
+ def updateInfo(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)
- titles = media['info']['titles']
+ # Update seasons
+ for season_num in seasons:
+ season_info = seasons[season_num]
+ episodes = season_info.get('episodes', {})
- if condense:
- # Use QueryCondenser to build a list of optimal search titles
- condensed_titles = self.query_condenser.distinct(titles)
+ # Find season that matches number
+ season = find(lambda s: s.get('info', {}).get('number', 0) == season_num, show_tree.get('seasons', []))
- 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 not season:
+ log.warning('Unable to find season "%s"', season_num)
+ continue
- if first:
- return titles[0] if titles else None
+ # Update season
+ fireEvent('show.season.update', season['_id'], info = season_info, single = True)
- return titles
+ # 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', episode['_id'], info = episode_info, single = True)
+
+ def updateExtras(self, media, info, store=False):
+ 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/_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/_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/episode.actions.js b/couchpotato/core/media/show/_base/static/episode.actions.js
new file mode 100755
index 0000000..cc46ce3
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/episode.actions.js
@@ -0,0 +1,474 @@
+var EpisodeAction = new Class({
+
+ Implements: [Options],
+
+ class_name: 'item-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.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
+ 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;
+ }
+ });
+
+ },
+
+ 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();
+
+ self.createReleases();
+
+ },
+
+ 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_table){
+ self.options.adopt(
+ self.releases_table = new Element('div.releases.table')
+ );
+
+ // 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_table);
+
+ 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': '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_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'))
+ 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_table.getElements('#release_'+self.last_release._id).addClass('last_release');
+
+ if(self.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_table, '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(){
+ self.close();
+ });
+ }
+
+ self.options.setStyle('height', self.releases_table.getSize().y)
+ .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_table.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.episode.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
new file mode 100755
index 0000000..92e49cc
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/episode.js
@@ -0,0 +1,128 @@
+var Episode = 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.episode').adopt(
+ self.detail = new Element('div.item.data')
+ );
+
+ self.create();
+ },
+
+ create: function(){
+ var self = this;
+
+ self.detail.set('id', 'episode_'+self.data._id);
+
+ 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', {
+ '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.titles && self.data.info.titles.length > 0) {
+ title = self.data.info.titles[0];
+ } else {
+ title = 'Episode ' + self.data.info.number;
+ }
+
+ 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/list.js b/couchpotato/core/media/show/_base/static/list.js
new file mode 100755
index 0000000..4177725
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/list.js
@@ -0,0 +1,636 @@
+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.list'),
+ 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.createShow(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.createShow(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) + ' shows');
+
+ 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 shows 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);
+
+ }
+
+ },
+
+ 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)
+ }, show);
+
+ $(m).inject(self.movie_list, inject_at || 'bottom');
+
+ m.fireEvent('injected');
+
+ self.movies.include(m);
+ self.movies_added[show._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({
+ 'type': 'show',
+ '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.shows);
+ self.addMovies(json.shows, json.total || json.shows.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/search.js b/couchpotato/core/media/show/_base/static/search.js
old mode 100644
new mode 100755
index 91e2ed3..96498e5
--- 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);
});
@@ -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)
});
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
new file mode 100755
index 0000000..ec20240
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/show.css
@@ -0,0 +1,1215 @@
+.shows.details_list {
+ 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 .list .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 .list .show.expanded {
+ height: 360px;
+ }
+
+ .shows .list .show .table.expanded {
+ height: 360px;
+ }
+
+ .shows.mass_edit_list .show {
+ padding-left: 22px;
+ background: none;
+ }
+
+ .shows.details_list .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 .list .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 .list .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 .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;
+ 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 .list .show:hover .data .actions,
+ .touch_enabled .shows .list .show .data .actions {
+ opacity: 1;
+ display: inline-block;
+ }
+
+ .shows.details_list .data .actions {
+ top: auto;
+ bottom: 18px;
+ }
+
+ .shows .list .show:hover .actions {
+ opacity: 1;
+ display: inline-block;
+ }
+ .shows.thumbs_list .data .actions {
+ bottom: 12px;
+ right: 10px;
+ top: auto;
+ }
+
+ .shows .list .show:hover .action { opacity: 0.6; }
+ .shows .list .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):not(.data):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 .list .episodes .item {
+ position: relative;
+ width: 100%;
+ height: auto;
+ padding: 0;
+
+ text-align: left;
+
+ 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;
+ }
+
+ .shows .list .episodes .item.data span.name {
+ width: 280px;
+ }
+
+ .shows .list .episodes .item.data span.firstaired {
+ width: 80px;
+ }
+
+ .shows .list .episodes .item.data span.quality {
+ bottom: auto;
+ }
+
+ .shows .list .episodes .item.season:hover {
+ background: none !important;
+ }
+
+ .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.6;
+ }
+
+ .shows .list .episodes .item.season:hover .data span.quality {
+ opacity: 1;
+ }
+
+ .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 {
+ width: 100%;
+ height: auto;
+ padding: 0;
+
+ background: rgba(0,0,0,.2);
+ }
+
+ .shows .list .episodes .releases.table span.name {
+ width: 300px;
+ }
+
+ .shows .list .episodes .releases.table span.status {
+ width: 85px;
+ }
+
+ .shows .list .episodes .item-actions {
+ position: absolute;
+ width: auto;
+ right: 0;
+
+ display: none;
+ opacity: 0;
+
+ border-left: none;
+ }
+
+ .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;
+ }
+
+ .shows .list .show .try_container {
+ padding: 5px 10px;
+ text-align: center;
+ }
+
+ .shows .list .show .try_container a {
+ margin: 0 5px;
+ padding: 2px 5px;
+ }
+
+ .shows .list .show .releases .next_release {
+ border-left: 6px solid #2aa300;
+ }
+
+ .shows .list .show .releases .next_release > :first-child {
+ margin-left: -6px;
+ }
+
+ .shows .list .show .releases .last_release {
+ border-left: 6px solid #ffa200;
+ }
+
+ .shows .list .show .releases .last_release > :first-child {
+ margin-left: -6px;
+ }
+ .shows .list .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 .list .show .trynext {
+ display: none;
+ }
+
+ @media all and (max-width: 480px) {
+ .shows .list .show .trynext {
+ display: none;
+ }
+ }
+ .shows.mass_edit_list .trynext { display: none; }
+ .wanted .shows .list .show .trynext {
+ padding-right: 30px;
+ }
+ .shows .list .show:hover .trynext,
+ .touch_enabled .shows.details_list .list .show .trynext {
+ opacity: 1;
+ }
+
+ .shows.details_list .list .show .trynext {
+ background: #47515f;
+ padding: 0;
+ right: 0;
+ height: 25px;
+ }
+
+ .shows .list .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 .list .show .trynext a:before {
+ margin: 2px 0 0 -20px;
+ position: absolute;
+ font-family: 'Elusive-Icons';
+ }
+ .shows.details_list .list .show .trynext a {
+ line-height: 23px;
+ }
+ .shows .list .show .trynext a:last-child {
+ margin: 0;
+ }
+ .shows .list .show .trynext a:hover,
+ .touch_enabled .shows .list .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..c622e74
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/show.episodes.js
@@ -0,0 +1,92 @@
+var Episodes = new Class({
+ initialize: function(show, options) {
+ var self = this;
+
+ self.show = show;
+ self.options = options;
+ },
+
+ 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, true);
+ },
+
+ createEpisodes: function() {
+ var self = this;
+
+ self.data.seasons.sort(self.sortSeasons);
+ self.data.seasons.each(function(season) {
+ self.createSeason(season);
+
+ season.episodes.sort(self.sortEpisodes);
+ season.episodes.each(function(episode) {
+ self.createEpisode(episode);
+ });
+ });
+ },
+
+ createSeason: function(season) {
+ var self = this,
+ s = new Season(self.show, self.options, season);
+
+ $(s).inject(self.episodes_container);
+ },
+
+ createEpisode: function(episode){
+ var self = this,
+ e = new Episode(self.show, self.options, episode);
+
+ $(e).inject(self.episodes_container);
+ },
+
+ sortSeasons: function(a, b) {
+ // Move "Specials" to the bottom of the list
+ if(!a.info.number) {
+ return 1;
+ }
+
+ if(!b.info.number) {
+ return -1;
+ }
+
+ // Order seasons descending
+ if(a.info.number < b.info.number)
+ return -1;
+
+ if(a.info.number > b.info.number)
+ return 1;
+
+ return 0;
+ },
+
+ 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
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..6019721
--- /dev/null
+++ b/couchpotato/core/media/show/_base/static/show.js
@@ -0,0 +1,370 @@
+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, {
+ 'actions': [EA.IMDB, EA.Release, EA.Refresh]
+ });
+
+ 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'));
+
+ 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(){
+ 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.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){
+ 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, expand){
+ 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();
+
+
+ if(expand === true) {
+ 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;
+ }
+
+});
diff --git a/couchpotato/core/media/show/episode.py b/couchpotato/core/media/show/episode.py
deleted file mode 100644
index c4a0a1a..0000000
--- a/couchpotato/core/media/show/episode.py
+++ /dev/null
@@ -1,147 +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('media.search_query', self.query)
- addEvent('media.identifier', self.identifier)
-
- addEvent('show.episode.add', self.add)
- addEvent('show.episode.update_info', self.updateInfo)
-
- def add(self, parent_id, info = None, update_after = True):
- if not info: info = {}
-
- identifiers = info.get('identifiers')
- try: del info['identifiers']
- except: pass
-
- # Add Season
- episode_info = {
- '_t': 'media',
- 'type': 'episode',
- 'identifiers': identifiers,
- '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.season.update_info', episode.get('_id'), info = info, single = True)
-
- return episode
-
- def updateInfo(self, media_id = None, info = None, force = False):
- if not info: info = {}
-
- if self.shuttingDown():
- return
-
- db = get_db()
-
- episode = db.get('id', media_id)
-
- # Get new info
- if not info:
- info = fireEvent('episode.info', episode.get('identifiers'), 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)
-
- # Get images
- image_urls = info.get('images', [])
- existing_files = episode.get('files', {})
- 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 100755
index 0000000..26b5c3d
--- /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') != 'show.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') != 'show.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 100755
index 0000000..89489af
--- /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') != 'show.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') != 'show.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/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.py b/couchpotato/core/media/show/matcher/base.py
old mode 100644
new mode 100755
similarity index 55%
rename from couchpotato/core/media/show/matcher.py
rename to couchpotato/core/media/show/matcher/base.py
index 4056d64..186334f
--- a/couchpotato/core/media/show/matcher.py
+++ b/couchpotato/core/media/show/matcher/base.py
@@ -1,19 +1,9 @@
-from couchpotato import CPLog
-from couchpotato.core.event import addEvent, fireEvent
-from couchpotato.core.helpers.variable import tryInt
+from couchpotato import fireEvent, CPLog, tryInt
+from couchpotato.core.event import addEvent
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):
@@ -80,51 +70,3 @@ class Base(MatcherBase):
identifier[key] = tryInt(value, value)
return identifier
-
-
-class Episode(Base):
- type = '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('media.identifier', media['library'], 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 = '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('media.identifier', media['library'], 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/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
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'
diff --git a/couchpotato/core/media/show/providers/info/thetvdb.py b/couchpotato/core/media/show/providers/info/thetvdb.py
index b2e58d9..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,56 +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()
-
- search_string = simplifyString(q)
- cache_key = 'thetvdb.cache.search.%s.%s' % (search_string, limit)
- results = self.getCache(cache_key)
-
- if not results:
- log.debug('Searching for show: %s', q)
-
- raw = None
- try:
- raw = self.tvdb.search(search_string)
- except (tvdb_exceptions.tvdb_error, IOError), e:
- log.error('Failed searching TheTVDB for "%s": %s', (search_string, 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:
@@ -129,22 +76,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:
@@ -158,12 +100,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
@@ -172,21 +114,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 = params.get('season_identifier', None)
- episode_identifier = 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 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:
@@ -200,20 +143,26 @@ 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():
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):
#
@@ -397,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)
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..f8c003c
--- 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):
'''
@@ -75,76 +77,69 @@ 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, 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_num = params.get('episode_number', None)
+ if episode_num is None:
return False
- season_identifier = params.get('season_identifier', None)
- if season_identifier is None:
+ season_num = params.get('season_number', None)
+ if season_num is None:
return False
- episode_identifier = params.get('episode_identifier', None)
- absolute = params.get('absolute', None)
-
- # season_identifier must contain the 'show id : season number' since there is no tvdb id
- # for season and we need a reference to both the show id and season number
- if season_identifier:
- try:
- identifier, season_identifier = season_identifier.split(':')
- season = int(season_identifier)
- except: return False
+ result = self.getShowInfo(identifiers)
- result = self.getShowInfo(identifier)
- map = {}
- if result:
- map_episode = result.get('map_episode', {}).get(season, {}).get(episode, {})
- if map_episode:
- map.update({'map_episode': map_episode})
+ if not result:
+ return False
- if absolute:
- map_absolute = result.get('map_absolute', {}).get(absolute, {})
- if map_absolute:
- map.update({'map_absolute': map_absolute})
+ # Find season
+ if season_num not in result['seasons']:
+ return False
- map_names = result.get('map_names', {}).get(toUnicode(season), {})
- 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 _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 +147,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
+
+ 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] = {}
- return map
+ season = result['seasons'][season]
+ season['title_map'] = title_map
def isDisabled(self):
if __name__ == '__main__':
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
})
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 100755
index 0000000..ea3a9db
--- /dev/null
+++ b/couchpotato/core/media/show/searcher/episode.py
@@ -0,0 +1,152 @@
+from couchpotato import fireEvent, get_db, Env
+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._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.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)
+
+ 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') != 'show.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 100755
index 0000000..c51d584
--- /dev/null
+++ b/couchpotato/core/media/show/searcher/season.py
@@ -0,0 +1,172 @@
+from couchpotato import get_db, Env
+from couchpotato.api import addApiView
+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__)
+
+autoload = 'SeasonSearcher'
+
+
+class SeasonSearcher(SearcherBase, ShowTypeBase):
+ type = 'season'
+
+ in_progress = False
+
+ def __init__(self):
+ super(SeasonSearcher, 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 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, 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)
+
+ # Find 'active' episodes
+ episodes = related['episodes']
+ episodes_active = []
+
+ 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
+
+ # 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
new file mode 100755
index 0000000..b7619cc
--- /dev/null
+++ b/couchpotato/core/media/show/searcher/show.py
@@ -0,0 +1,88 @@
+from couchpotato import get_db
+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
+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.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:
+ 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)
+
+ 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)
+
+ for season in show_tree.get('seasons', []):
+ if not season.get('info'):
+ 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
+
+ # Check if full season can be downloaded
+ fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
+
+ # TODO (testing) only snatch one season
+ 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)
diff --git a/couchpotato/core/media/show/season.py b/couchpotato/core/media/show/season.py
deleted file mode 100644
index 9c6f8f0..0000000
--- a/couchpotato/core/media/show/season.py
+++ /dev/null
@@ -1,137 +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('media.search_query', self.query)
- addEvent('media.identifier', self.identifier)
-
- addEvent('show.season.add', self.add)
- addEvent('show.season.update_info', self.updateInfo)
-
- def add(self, parent_id, info = None, update_after = True):
- if not info: info = {}
-
- identifiers = info.get('identifiers')
- try: del info['identifiers']
- except: pass
- try: del info['episodes']
- except: pass
-
- # Add Season
- season_info = {
- '_t': 'media',
- 'type': 'season',
- 'identifiers': identifiers,
- '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_info', season.get('_id'), info = info, single = True)
-
- return season
-
- def updateInfo(self, media_id = None, info = None, force = False):
- if not info: info = {}
-
- if self.shuttingDown():
- return
-
- db = get_db()
-
- season = db.get('id', media_id)
-
- # Get new info
- if not info:
- info = fireEvent('season.info', season.get('identifiers'), 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)
-
- # Get images
- image_urls = info.get('images', [])
- existing_files = season.get('files', {})
- 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)
- }
diff --git a/couchpotato/core/plugins/dashboard.py b/couchpotato/core/plugins/dashboard.py
old mode 100644
new mode 100755
diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py
index 3ed1a7a..aaaed9b 100644
--- a/couchpotato/core/plugins/quality/main.py
+++ b/couchpotato/core/plugins/quality/main.py
@@ -33,7 +33,25 @@ class QualityPlugin(Plugin):
{'identifier': 'r5', 'size': (600, 1000), 'median_size': 700, 'label': 'R5', 'alternative': ['r6'], 'allow': ['dvdr', '720p', '1080p'], 'ext':[]},
{'identifier': 'tc', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': ['720p', '1080p'], 'ext':[]},
{'identifier': 'ts', 'size': (600, 1000), 'median_size': 700, 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': ['720p', '1080p'], 'ext':[]},
- {'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], 'ext':[]}
+ {'identifier': 'cam', 'size': (600, 1000), 'median_size': 700, 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': ['720p', '1080p'], '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 = {
diff --git a/couchpotato/core/plugins/score/main.py b/couchpotato/core/plugins/score/main.py
old mode 100644
new mode 100755
index e6fef25..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']['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'])
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