from couchpotato import get_session , Env
from couchpotato . core . event import addEvent , fireEvent
from couchpotato . core . helpers . variable import getTitle , tryInt , possibleTitles
from couchpotato . core . logger import CPLog
from couchpotato . core . media . _base . searcher . main import SearchSetupError
from couchpotato . core . plugins . base import Plugin
from couchpotato . core . settings . model import Media , Library
from caper import Caper
log = CPLog ( __name__ )
class ShowSearcher ( Plugin ) :
in_progress = False
# TODO come back to this later, think this could be handled better
quality_map = {
' webdl_1080p ' : { ' resolution ' : [ ' 1080p ' ] , ' source ' : [ ' webdl ' ] } ,
' webdl_720p ' : { ' resolution ' : [ ' 720p ' ] , ' source ' : [ ' webdl ' ] } ,
' hdtv_720p ' : { ' resolution ' : [ ' 720p ' ] , ' source ' : [ ' hdtv ' ] } ,
' hdtv_sd ' : { ' resolution ' : [ ' 480p ' , None ] , ' source ' : [ ' hdtv ' ] } ,
}
def __init__ ( self ) :
super ( ShowSearcher , self ) . __init__ ( )
addEvent ( ' show.searcher.single ' , self . single )
addEvent ( ' searcher.correct_release ' , self . correctRelease )
addEvent ( ' searcher.get_search_title ' , self . getSearchTitle )
self . caper = Caper ( )
def _lookupMedia ( self , media ) :
db = get_session ( )
media_library = db . query ( Library ) . filter_by ( id = media [ ' library_id ' ] ) . first ( )
show = None
season = None
episode = None
if media [ ' type ' ] == ' episode ' :
show = media_library . parent . parent
season = media_library . parent
episode = media_library
if media [ ' type ' ] == ' season ' :
show = media_library . parent
season = media_library
if media [ ' type ' ] == ' show ' :
show = media_library
return show , season , episode
def single ( self , media , search_protocols = None , manual = False ) :
if media [ ' type ' ] == ' show ' :
# TODO handle show searches (scan all seasons)
return
# Find out search type
try :
if not search_protocols :
search_protocols = fireEvent ( ' searcher.protocols ' , single = True )
except SearchSetupError :
return
done_status = fireEvent ( ' status.get ' , ' done ' , single = True )
if not media [ ' profile ' ] or media [ ' status_id ' ] == done_status . get ( ' id ' ) :
log . debug ( ' Episode doesn \' t have a profile or already done, assuming in manage tab. ' )
return
db = get_session ( )
pre_releases = fireEvent ( ' quality.pre_releases ' , single = True )
available_status , ignored_status , failed_status = fireEvent ( ' status.get ' , [ ' available ' , ' ignored ' , ' failed ' ] , single = True )
found_releases = [ ]
too_early_to_search = [ ]
default_title = self . getSearchTitle ( media )
if not default_title :
log . error ( ' No proper info found for episode, removing it from library to cause it from having more issues. ' )
#fireEvent('episode.delete', episode['id'], single = True)
return
show , season , episode = self . _lookupMedia ( media )
if show is None or season is None :
log . error ( ' Unable to find show or season library in database, missing required data for searching ' )
return
fireEvent ( ' notify.frontend ' , type = ' show.searcher.started. %s ' % media [ ' id ' ] , data = True , message = ' Searching for " %s " ' % default_title )
ret = False
for quality_type in media [ ' profile ' ] [ ' types ' ] :
# 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'])
# continue
has_better_quality = 0
# See if better quality is available
for release in media [ ' releases ' ] :
if release [ ' quality ' ] [ ' order ' ] < = quality_type [ ' quality ' ] [ ' order ' ] and release [ ' status_id ' ] not in [ available_status . get ( ' id ' ) , ignored_status . get ( ' id ' ) , failed_status . get ( ' id ' ) ] :
has_better_quality + = 1
# Don't search for quality lower then already available.
if has_better_quality is 0 :
log . info ( ' Search for %s S %02d %s in %s ' , ( getTitle ( show ) , season . season_number , " E %02d " % episode . episode_number if episode else " " , quality_type [ ' quality ' ] [ ' label ' ] ) )
quality = fireEvent ( ' quality.single ' , identifier = quality_type [ ' quality ' ] [ ' identifier ' ] , single = True )
results = fireEvent ( ' searcher.search ' , search_protocols , media , quality , single = True )
if len ( results ) == 0 :
log . debug ( ' Nothing found for %s in %s ' , ( default_title , quality_type [ ' quality ' ] [ ' label ' ] ) )
# Check if movie isn't deleted while searching
if not db . query ( Media ) . filter_by ( id = media . get ( ' id ' ) ) . first ( ) :
break
# Add them to this movie releases list
found_releases + = fireEvent ( ' searcher.create_releases ' , results , media , quality_type , single = True )
# Try find a valid result and download it
if fireEvent ( ' searcher.try_download_result ' , results , media , quality_type , manual , single = True ) :
ret = True
# Remove releases that aren't found anymore
for release in media . get ( ' releases ' , [ ] ) :
if release . get ( ' status_id ' ) == available_status . get ( ' id ' ) 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 ' , ( quality_type [ ' quality ' ] [ ' label ' ] , default_title ) )
fireEvent ( ' movie.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 , default_title ) )
fireEvent ( ' notify.frontend ' , type = ' show.searcher.ended. %s ' % media [ ' id ' ] , data = True )
return ret
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
show , season , episode = self . _lookupMedia ( media )
if show is None or season is None :
log . error ( ' Unable to find show or season library in database, missing required data for searching ' )
return
release_info = self . caper . parse ( release [ ' name ' ] )
if len ( release_info . chains ) < 1 :
log . info2 ( ' Wrong: %s , unable to parse release name (no chains) ' , release [ ' name ' ] )
return False
# TODO look at all chains
chain = release_info . chains [ 0 ]
if not self . correctQuality ( chain , quality [ ' identifier ' ] ) :
log . info ( ' Wrong: %s , quality does not match ' , release [ ' name ' ] )
return False
if not self . correctIdentifier ( chain , media ) :
log . info ( ' Wrong: %s , identifier does not match ' , release [ ' name ' ] )
return False
if ' show_name ' not in chain . info or not len ( chain . info [ ' show_name ' ] ) :
log . info ( ' Wrong: %s , missing show name in parsed result ' , release [ ' name ' ] )
return False
chain_words = [ x . lower ( ) for x in chain . info [ ' show_name ' ] ]
chain_title = ' ' . join ( chain_words )
library_title = None
# Check show titles match
for raw_title in show . titles :
for valid_words in [ x . split ( ' ' ) for x in possibleTitles ( raw_title . title ) ] :
if not library_title :
library_title = ' ' . join ( valid_words )
if valid_words == chain_words :
return chain . weight
log . info ( " Wrong: title ' %s ' , undetermined show naming. Looking for ' %s ( %s ) ' " , ( chain_title , library_title , media [ ' library ' ] [ ' year ' ] ) )
return False
def correctQuality ( self , chain , quality_identifier ) :
if quality_identifier not in self . quality_map :
log . info2 ( ' Wrong: unknown preferred quality %s for TV searching ' , quality_identifier )
return False
if ' video ' not in chain . info :
log . info2 ( ' Wrong: no video tags found ' )
return False
video_tags = self . quality_map [ quality_identifier ]
if not self . chainMatches ( chain , ' video ' , video_tags ) :
log . info2 ( ' Wrong: %s tags not in chain ' , video_tags )
return False
return True
def correctIdentifier ( self , chain , media ) :
required_id = self . getMediaIdentifier ( media [ ' library ' ] )
if ' identifier ' not in chain . info :
return False
# TODO could be handled better?
if len ( chain . info [ ' identifier ' ] ) != 1 :
return False
identifier = chain . info [ ' identifier ' ] [ 0 ]
# TODO air by date episodes
release_id = self . toNumericIdentifier ( identifier . get ( ' season ' ) , identifier . get ( ' episode ' ) )
if required_id != release_id :
log . info2 ( ' Wrong: required identifier %s does not match release identifier %s ' , ( str ( required_id ) , str ( release_id ) ) )
return False
return True
def getMediaIdentifier ( self , media_library ) :
identifier = None , None
if media_library [ ' type ' ] == ' episode ' :
map_episode = media_library [ ' info ' ] . get ( ' map_episode ' )
if map_episode and ' scene ' in map_episode :
identifier = (
map_episode [ ' scene ' ] . get ( ' season ' ) ,
map_episode [ ' scene ' ] . get ( ' episode ' )
)
else :
# TODO xem mapping?
identifier = (
media_library . get ( ' season_number ' ) ,
media_library . get ( ' episode_number ' )
)
if media_library [ ' type ' ] == ' season ' :
identifier = media_library . get ( ' season_number ' ) , None
return self . toNumericIdentifier ( * identifier )
def toNumericIdentifier ( self , season , episode ) :
return tryInt ( season , None ) , tryInt ( episode , None )
def chainMatches ( self , chain , group , tags ) :
found_tags = [ ]
for match in chain . info [ group ] :
for ck , cv in match . items ( ) :
if ck in tags and self . cleanMatchValue ( cv ) in tags [ ck ] :
found_tags . append ( ck )
if set ( tags . keys ( ) ) == set ( found_tags ) :
return True
return set ( [ key for key , value in tags . items ( ) if None not in value ] ) == set ( found_tags )
def cleanMatchValue ( self , value ) :
value = value . lower ( )
value = value . strip ( )
for ch in [ ' ' , ' - ' , ' . ' ] :
value = value . replace ( ch , ' ' )
return value
def getSearchTitle ( self , media ) :
show , season , episode = self . _lookupMedia ( media )
if show is None :
return None
# TODO this misses alternative titles from the database
show_title = getTitle ( show )
if not show_title :
return None
season_num , episode_num = self . getMediaIdentifier ( media [ ' library ' ] )
name = show_title
if season_num :
name + = ' S %02d ' % season_num
if episode_num :
name + = ' E %02d ' % episode_num
return name