Browse Source

Merge branch 'tv_season_searcher' into tv

pull/3730/merge
Dean Gardiner 11 years ago
parent
commit
6f620f451b
  1. 21
      couchpotato/core/media/_base/library/main.py
  2. 6
      couchpotato/core/media/show/_base/main.py
  3. 2
      couchpotato/core/media/show/_base/static/episode.actions.js
  4. 6
      couchpotato/core/media/show/_base/static/episode.js
  5. 127
      couchpotato/core/media/show/_base/static/season.js
  6. 65
      couchpotato/core/media/show/_base/static/show.css
  7. 16
      couchpotato/core/media/show/_base/static/show.episodes.js
  8. 3
      couchpotato/core/media/show/matcher.py
  9. 2
      couchpotato/core/media/show/searcher/episode.py
  10. 142
      couchpotato/core/media/show/searcher/season.py
  11. 32
      couchpotato/core/media/show/searcher/show.py
  12. 19
      couchpotato/core/plugins/score/main.py

21
couchpotato/core/media/_base/library/main.py

@ -55,7 +55,7 @@ class Library(LibraryBase):
) )
def related(self, media): def related(self, media):
result = {media['type']: media} result = {self.key(media['type']): media}
db = get_db() db = get_db()
cur = media cur = media
@ -63,9 +63,17 @@ class Library(LibraryBase):
while cur and cur.get('parent_id'): while cur and cur.get('parent_id'):
cur = db.get('id', cur['parent_id']) cur = db.get('id', cur['parent_id'])
parts = cur['type'].split('.') result[self.key(cur['type'])] = cur
result[parts[-1]] = cur children = db.get_many('media_children', media['_id'], with_doc = True)
for item in children:
key = self.key(item['doc']['type']) + 's'
if key not in result:
result[key] = []
result[key].append(item['doc'])
return result return result
@ -94,8 +102,7 @@ class Library(LibraryBase):
# Build children arrays # Build children arrays
for item in items: for item in items:
parts = item['doc']['type'].split('.') key = self.key(item['doc']['type']) + 's'
key = parts[-1] + 's'
if key not in result: if key not in result:
result[key] = {} result[key] = {}
@ -115,3 +122,7 @@ class Library(LibraryBase):
result['releases'] = fireEvent('release.for_media', result['_id'], single = True) result['releases'] = fireEvent('release.for_media', result['_id'], single = True)
return result return result
def key(self, media_type):
parts = media_type.split('.')
return parts[-1]

6
couchpotato/core/media/show/_base/main.py

@ -4,11 +4,9 @@ import traceback
from couchpotato import get_db from couchpotato import get_db
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import getTitle, find from couchpotato.core.helpers.variable import getTitle, find
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from qcond import QueryCondenser
log = CPLog(__name__) log = CPLog(__name__)
@ -110,7 +108,7 @@ class ShowBase(MediaBase):
new = False new = False
try: try:
m = fireEvent('media.with_identifiers', params.get('identifiers'), with_doc = True, single = True)['doc'] m = db.get('media', 'thetvdb-%s' % params.get('identifiers', {}).get('thetvdb'), with_doc = True)['doc']
except: except:
new = True new = True
m = db.insert(media) m = db.insert(media)
@ -155,7 +153,7 @@ class ShowBase(MediaBase):
# Trigger update info # Trigger update info
if added and update_after: if added and update_after:
# Do full update to get images etc # Do full update to get images etc
fireEventAsync('show.update_extras', m, info, store = True, on_complete = onComplete) fireEventAsync('show.update_extras', m.copy(), info, store = True, on_complete = onComplete)
# Remove releases # Remove releases
for rel in fireEvent('release.for_media', m['_id'], single = True): for rel in fireEvent('release.for_media', m['_id'], single = True):

2
couchpotato/core/media/show/_base/static/episode.actions.js

@ -2,7 +2,7 @@ var EpisodeAction = new Class({
Implements: [Options], Implements: [Options],
class_name: 'action icon2', class_name: 'item-action icon2',
initialize: function(episode, options){ initialize: function(episode, options){
var self = this; var self = this;

6
couchpotato/core/media/show/_base/static/episode.js

@ -14,7 +14,7 @@ var Episode = new Class({
self.profile = self.show.profile; self.profile = self.show.profile;
self.el = new Element('div.item').adopt( self.el = new Element('div.item.episode').adopt(
self.detail = new Element('div.item.data') self.detail = new Element('div.item.data')
); );
@ -34,14 +34,14 @@ var Episode = new Class({
self.quality = new Element('span.quality', { self.quality = new Element('span.quality', {
'events': { 'events': {
'click': function(e){ 'click': function(e){
var releases = self.detail.getElement('.episode-actions .releases'); var releases = self.detail.getElement('.item-actions .releases');
if(releases.isVisible()) if(releases.isVisible())
releases.fireEvent('click', [e]) releases.fireEvent('click', [e])
} }
} }
}), }),
self.actions = new Element('div.episode-actions') self.actions = new Element('div.item-actions')
); );
// Add profile // Add profile

127
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]
}
});

65
couchpotato/core/media/show/_base/static/show.css

@ -616,7 +616,7 @@
.shows .options .table .item:nth-child(even) { .shows .options .table .item:nth-child(even) {
background: rgba(255,255,255,0.05); background: rgba(255,255,255,0.05);
} }
.shows .options .table .item:not(.head):hover { .shows .options .table .item:not(.head):not(.data):hover {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
} }
@ -717,6 +717,10 @@
transition: all .6s cubic-bezier(0.9,0,0.1,1); 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 { .shows .list .episodes .item.data span.episode {
width: 40px; width: 40px;
padding: 0 10px; padding: 0 10px;
@ -734,6 +738,31 @@
bottom: auto; 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 { .shows .list .episodes .episode-options {
display: block; display: block;
@ -765,16 +794,44 @@
width: 85px; width: 85px;
} }
.shows .list .episodes .episode-actions { .shows .list .episodes .item-actions {
position: absolute; position: absolute;
width: auto; width: auto;
right: 0; right: 0;
top: 0;
display: none;
opacity: 0;
border-left: none; border-left: none;
} }
.shows .list .show .episodes .episode-actions .refresh { .shows .list .episodes .item:hover .item-actions {
display: inline-block;
opacity: 1;
}
.shows .list .episodes .item:hover .item-action {
opacity: 0.6;
}
.shows .list .episodes .item .item-action {
display: inline-block;
height: 22px;
min-width: 33px;
line-height: 26px;
text-align: center;
font-size: 13px;
margin-left: 1px;
padding: 0 5px;
}
.shows .list .episodes .item .item-action:hover {
opacity: 1;
}
.shows .list .show .episodes .refresh {
color: #cbeecc; color: #cbeecc;
} }

16
couchpotato/core/media/show/_base/static/show.episodes.js

@ -47,21 +47,9 @@ var Episodes = new Class({
createSeason: function(season) { createSeason: function(season) {
var self = this, var self = this,
title = ''; s = new Season(self.show, self.options, season);
if(season.info.number) { $(s).inject(self.episodes_container);
title = 'Season ' + season.info.number;
} else {
// Season 0 / Specials
title = 'Specials';
}
season['el'] = new Element('div', {
'class': 'item head',
'id': 'season_'+season._id
}).adopt(
new Element('span.name', {'text': title})
).inject(self.episodes_container);
}, },
createEpisode: function(episode){ createEpisode: function(episode){

3
couchpotato/core/media/show/matcher.py

@ -107,6 +107,7 @@ class Episode(Base):
return True return True
class Season(Base): class Season(Base):
type = 'show.season' type = 'show.season'
@ -121,7 +122,7 @@ class Season(Base):
log.info2('Wrong: releases with identifier ranges are not supported yet') log.info2('Wrong: releases with identifier ranges are not supported yet')
return False return False
required = fireEvent('media.identifier', media['library'], single = True) required = fireEvent('library.identifier', media, single = True)
if identifier != required: if identifier != required:
log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier)) log.info2('Wrong: required identifier (%s) does not match release identifier (%s)', (required, identifier))

2
couchpotato/core/media/show/searcher/episode.py

@ -117,7 +117,7 @@ class EpisodeSearcher(SearcherBase, ShowTypeBase):
# Remove releases that aren't found anymore # Remove releases that aren't found anymore
for release in releases: for release in releases:
if release.get('status') == 'available' and release.get('identifier') not in found_releases: if release.get('status') == 'available' and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('id'), single = True) fireEvent('release.delete', release.get('_id'), single = True)
else: else:
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query)) log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
fireEvent('media.restatus', media['_id']) fireEvent('media.restatus', media['_id'])

142
couchpotato/core/media/show/searcher/season.py

@ -1,7 +1,9 @@
from couchpotato import get_db, Env
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie.searcher import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__) log = CPLog(__name__)
@ -19,6 +21,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
addEvent('%s.searcher.all' % self.getType(), self.searchAll) addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single) addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = { addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted seasons', 'desc': 'Starts a full search for all wanted seasons',
@ -34,21 +37,136 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
def searchAll(self, manual = False): def searchAll(self, manual = False):
pass pass
def single(self, media, show, profile): def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
# Check if any episode is already snatched if not profile and related['show']['profile_id']:
active = 0 profile = db.get('id', related['show']['profile_id'])
episodes = media.get('episodes', {})
for ex in episodes: if not quality_order:
episode = episodes.get(ex) quality_order = fireEvent('quality.order', single = True)
if episode.get('status') in ['active']: # Find 'active' episodes
active += 1 episodes = related['episodes']
episodes_active = []
if active != len(episodes): for episode in episodes:
if episode.get('status') != 'active':
continue
episodes_active.append(episode)
if len(episodes_active) == len(episodes):
# All episodes are 'active', try and search for full season
if self.search(media, profile, quality_order, search_protocols):
# Success, end season search
return True
else:
log.info('Unable to find season pack, searching for individual episodes...')
# Search for each episode individually
for episode in episodes_active:
fireEvent('show.episode.searcher.single', episode, profile, quality_order, search_protocols, manual)
# TODO (testing) only grab one episode
return True
return True
def search(self, media, profile, quality_order, search_protocols):
# TODO: check episode status
# TODO: check air date
#if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates, movie['library']['year']):
# too_early_to_search.append(quality_type['quality']['identifier'])
# return
ret = False
has_better_quality = None
found_releases = []
too_early_to_search = []
releases = fireEvent('release.for_media', media['_id'], single = True)
query = fireEvent('library.query', media, condense = False, single = True)
index = 0
for q_identifier in profile.get('qualities'):
quality_custom = {
'quality': q_identifier,
'finish': profile['finish'][index],
'wait_for': profile['wait_for'][index],
'3d': profile['3d'][index] if profile.get('3d') else False
}
has_better_quality = 0
# See if better quality is available
for release in releases:
if quality_order.index(release['quality']) <= quality_order.index(q_identifier) and release['status'] not in ['available', 'ignored', 'failed']:
has_better_quality += 1
# Don't search for quality lower then already available.
if has_better_quality is 0:
log.info('Searching for %s in %s', (query, q_identifier))
quality = fireEvent('quality.single', identifier = q_identifier, single = True)
quality['custom'] = quality_custom
results = fireEvent('searcher.search', search_protocols, media, quality, single = True)
if len(results) == 0:
log.debug('Nothing found for %s in %s', (query, q_identifier))
# Add them to this movie releases list
found_releases += fireEvent('release.create_from_search', results, media, quality, single = True)
# Try find a valid result and download it
if fireEvent('release.try_download_result', results, media, quality, single = True):
ret = True
# Remove releases that aren't found anymore
for release in releases:
if release.get('status') == 'available' and release.get('identifier') not in found_releases:
fireEvent('release.delete', release.get('_id'), single = True)
else:
log.info('Better quality (%s) already available or snatched for %s', (q_identifier, query))
fireEvent('media.restatus', media['_id'])
break
# Break if CP wants to shut down
if self.shuttingDown() or ret:
break
if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, query))
return len(found_releases) > 0
def correctRelease(self, release = None, media = None, quality = None, **kwargs):
if media.get('type') != 'show.season':
return
retention = Env.setting('retention', section = 'nzb')
if release.get('seeders') is None and 0 < retention < release.get('age', 0):
log.info2('Wrong: Outside retention, age is %s, needs %s or lower: %s', (release['age'], retention, release['name']))
return False
# Check for required and ignored words
if not fireEvent('searcher.correct_words', release['name'], media, single = True):
return False return False
# Try and search for full season # TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
# TODO: match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False return False

32
couchpotato/core/media/show/searcher/show.py

@ -54,36 +54,26 @@ class ShowSearcher(SearcherBase, ShowTypeBase):
fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title) fireEvent('notify.frontend', type = 'show.searcher.started.%s' % media['_id'], data = True, message = 'Searching for "%s"' % show_title)
media = self.extendShow(media) show_tree = fireEvent('library.tree', media, single = True)
db = get_db() db = get_db()
profile = db.get('id', media['profile_id']) profile = db.get('id', media['profile_id'])
quality_order = fireEvent('quality.order', single = True) quality_order = fireEvent('quality.order', single = True)
seasons = media.get('seasons', {}) for season in show_tree.get('seasons', []):
for sx in seasons: if not season.get('info'):
continue
# Skip specials for now TODO: set status for specials to skipped by default # Skip specials (and seasons missing 'number') for now
if sx == 0: continue # TODO: set status for specials to skipped by default
if not season['info'].get('number'):
continue
season = seasons.get(sx) # Check if full season can be downloaded
fireEvent('show.season.searcher.single', season, profile, quality_order, search_protocols, manual)
# Check if full season can be downloaded TODO: add # TODO (testing) only snatch one season
season_success = fireEvent('show.season.searcher.single', season, media, profile)
# Do each episode seperately
if not season_success:
episodes = season.get('episodes', {})
for ex in episodes:
episode = episodes.get(ex)
fireEvent('show.episode.searcher.single', episode, season, media, profile, quality_order, search_protocols)
# TODO
return
# TODO
return return
fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True) fireEvent('notify.frontend', type = 'show.searcher.ended.%s' % media['_id'], data = True)

19
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.encoding import toUnicode
from couchpotato.core.helpers.variable import getTitle, splitString, removeDuplicate from couchpotato.core.helpers.variable import getTitle, splitString, removeDuplicate
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@ -16,17 +16,20 @@ class Score(Plugin):
def __init__(self): def __init__(self):
addEvent('score.calculate', self.calculate) 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 """ """ 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 # Merge global and category
preferred_words = splitString(Env.setting('preferred_words', section = 'searcher').lower()) 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 except: pass
score = nameScore(toUnicode(nzb['name']), movie['info'].get('year'), preferred_words) score = nameScore(toUnicode(nzb['name']), root['info'].get('year'), preferred_words)
for movie_title in movie['info']['titles']: for movie_title in root['info']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title)) score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title))
score += namePositionScore(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']) score += providerScore(nzb['provider'])
# Duplicates in name # Duplicates in name
score += duplicateScore(nzb['name'], getTitle(movie)) score += duplicateScore(nzb['name'], getTitle(root))
# Merge global and category # Merge global and category
ignored_words = splitString(Env.setting('ignored_words', section = 'searcher').lower()) 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 except: pass
# Partial ignored words # Partial ignored words
score += partialIgnoredScore(nzb['name'], getTitle(movie), ignored_words) score += partialIgnoredScore(nzb['name'], getTitle(root), ignored_words)
# Ignore single downloads from multipart # Ignore single downloads from multipart
score += halfMultipartScore(nzb['name']) score += halfMultipartScore(nzb['name'])

Loading…
Cancel
Save