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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.core.event import addEvent, fireEventAsync
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase
from couchpotato.core.media.movie.searcher import SearchSetupError
from couchpotato.core.media.show import ShowTypeBase
log = CPLog(__name__)
@ -19,6 +21,7 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
addEvent('%s.searcher.all' % self.getType(), self.searchAll)
addEvent('%s.searcher.single' % self.getType(), self.single)
addEvent('searcher.correct_release', self.correctRelease)
addApiView('%s.searcher.full_search' % self.getType(), self.searchAllView, docs = {
'desc': 'Starts a full search for all wanted seasons',
@ -34,21 +37,136 @@ class SeasonSearcher(SearcherBase, ShowTypeBase):
def searchAll(self, manual = False):
pass
def single(self, media, show, profile):
def single(self, media, profile = None, quality_order = None, search_protocols = None, manual = False):
db = get_db()
related = fireEvent('library.related', media, single = True)
# TODO search_protocols, profile, quality_order can be moved to a base method
# Find out search type
try:
if not search_protocols:
search_protocols = fireEvent('searcher.protocols', single = True)
except SearchSetupError:
return
# Check if any episode is already snatched
active = 0
episodes = media.get('episodes', {})
for ex in episodes:
episode = episodes.get(ex)
if not profile and related['show']['profile_id']:
profile = db.get('id', related['show']['profile_id'])
if not quality_order:
quality_order = fireEvent('quality.order', single = True)
if episode.get('status') in ['active']:
active += 1
# Find 'active' episodes
episodes = 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
# Try and search for full season
# TODO:
# TODO Matching is quite costly, maybe we should be caching release matches somehow? (also look at caper optimizations)
match = fireEvent('matcher.match', release, media, quality, single = True)
if match:
return match.weight
return False

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

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.variable import getTitle, splitString, removeDuplicate
from couchpotato.core.logger import CPLog
@ -16,17 +16,20 @@ class Score(Plugin):
def __init__(self):
addEvent('score.calculate', self.calculate)
def calculate(self, nzb, movie):
def calculate(self, nzb, media):
""" Calculate the score of a NZB, used for sorting later """
# Fetch root media item (movie, show)
root = fireEvent('library.root', media, single = True)
# Merge global and category
preferred_words = splitString(Env.setting('preferred_words', section = 'searcher').lower())
try: preferred_words = removeDuplicate(preferred_words + splitString(movie['category']['preferred'].lower()))
try: preferred_words = removeDuplicate(preferred_words + splitString(media['category']['preferred'].lower()))
except: pass
score = nameScore(toUnicode(nzb['name']), movie['info'].get('year'), preferred_words)
score = nameScore(toUnicode(nzb['name']), root['info'].get('year'), preferred_words)
for movie_title in movie['info']['titles']:
for movie_title in root['info']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title))
score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title))
@ -44,15 +47,15 @@ class Score(Plugin):
score += providerScore(nzb['provider'])
# Duplicates in name
score += duplicateScore(nzb['name'], getTitle(movie))
score += duplicateScore(nzb['name'], getTitle(root))
# Merge global and category
ignored_words = splitString(Env.setting('ignored_words', section = 'searcher').lower())
try: ignored_words = removeDuplicate(ignored_words + splitString(movie['category']['ignored'].lower()))
try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower()))
except: pass
# Partial ignored words
score += partialIgnoredScore(nzb['name'], getTitle(movie), ignored_words)
score += partialIgnoredScore(nzb['name'], getTitle(root), ignored_words)
# Ignore single downloads from multipart
score += halfMultipartScore(nzb['name'])

Loading…
Cancel
Save