XEM Scene Numbering fixes.

Deepcopy thread.lock issue fixed.
Overall performance increased, searches faster and more accurate.
This commit is contained in:
echel0n 2014-04-26 03:37:40 -07:00
parent bf31077cc2
commit e787ef8332
22 changed files with 192 additions and 81 deletions

View file

@ -994,3 +994,123 @@ def real_path(path):
Returns: the canonicalized absolute pathname. The resulting path will have no symbolic link, '/./' or '/../' components.
"""
return ek.ek(os.path.normpath, ek.ek(os.path.normcase, ek.ek(os.path.realpath, path)))
def _copy(self, obj, objectmap=None):
"""
<Purpose>
Create a deep copy of an object without using the python 'copy' module.
Using copy.deepcopy() doesn't work because builtins like id and hasattr
aren't available when this is called.
<Arguments>
self
obj
The object to make a deep copy of.
objectmap
A mapping between original objects and the corresponding copy. This is
used to handle circular references.
<Exceptions>
TypeError
If an object is encountered that we don't know how to make a copy of.
NamespaceViolationError
If an unexpected error occurs while copying. This isn't the greatest
solution, but in general the idea is we just need to abort the wrapped
function call.
<Side Effects>
A new reference is created to every non-simple type of object. That is,
everything except objects of type str, unicode, int, etc.
<Returns>
The deep copy of obj with circular/recursive references preserved.
"""
try:
# If this is a top-level call to _copy, create a new objectmap for use
# by recursive calls to _copy.
if objectmap is None:
objectmap = {}
# If this is a circular reference, use the copy we already made.
elif _saved_id(obj) in objectmap:
return objectmap[_saved_id(obj)]
# types.InstanceType is included because the user can provide an instance
# of a class of their own in the list of callback args to settimer.
if _is_in(type(obj), [str, unicode, int, long, float, complex, bool, frozenset,
types.NoneType, types.FunctionType, types.LambdaType,
types.MethodType, types.InstanceType]):
return obj
elif type(obj) is list:
temp_list = []
# Need to save this in the objectmap before recursing because lists
# might have circular references.
objectmap[_saved_id(obj)] = temp_list
for item in obj:
temp_list.append(self._copy(item, objectmap))
return temp_list
elif type(obj) is tuple:
temp_list = []
for item in obj:
temp_list.append(self._copy(item, objectmap))
# I'm not 100% confident on my reasoning here, so feel free to point
# out where I'm wrong: There's no way for a tuple to directly contain
# a circular reference to itself. Instead, it has to contain, for
# example, a dict which has the same tuple as a value. In that
# situation, we can avoid infinite recursion and properly maintain
# circular references in our copies by checking the objectmap right
# after we do the copy of each item in the tuple. The existence of the
# dictionary would keep the recursion from being infinite because those
# are properly handled. That just leaves making sure we end up with
# only one copy of the tuple. We do that here by checking to see if we
# just made a copy as a result of copying the items above. If so, we
# return the one that's already been made.
if _saved_id(obj) in objectmap:
return objectmap[_saved_id(obj)]
retval = tuple(temp_list)
objectmap[_saved_id(obj)] = retval
return retval
elif type(obj) is set:
temp_list = []
# We can't just store this list object in the objectmap because it isn't
# a set yet. If it's possible to have a set contain a reference to
# itself, this could result in infinite recursion. However, sets can
# only contain hashable items so I believe this can't happen.
for item in obj:
temp_list.append(self._copy(item, objectmap))
retval = set(temp_list)
objectmap[_saved_id(obj)] = retval
return retval
elif type(obj) is dict:
temp_dict = {}
# Need to save this in the objectmap before recursing because dicts
# might have circular references.
objectmap[_saved_id(obj)] = temp_dict
for key, value in obj.items():
temp_key = self._copy(key, objectmap)
temp_dict[temp_key] = self._copy(value, objectmap)
return temp_dict
# We don't copy certain objects. This is because copying an emulated file
# object, for example, will cause the destructor of the original one to
# be invoked, which will close the actual underlying file. As the object
# is wrapped and the client does not have access to it, it's safe to not
# wrap it.
elif isinstance(obj, (NamespaceObjectWrapper, emulfile.emulated_file,
emulcomm.emulated_socket, thread.LockType,
virtual_namespace.VirtualNamespace)):
return obj
else:
raise TypeError("_copy is not implemented for objects of type " + str(type(obj)))
except Exception, e:
self._handle_violation("_copy failed on " + str(obj) + " with message " + str(e))

View file

@ -193,7 +193,7 @@ class NameParser(object):
if cached:
if fix_scene_numbering:
cached_fixed = copy.copy(cached)
cached_fixed = copy.deepcopy(cached)
cached_fixed.fix_scene_numbering()
return cached_fixed
return cached
@ -248,7 +248,7 @@ class NameParser(object):
name_parser_cache.add(name, final_result)
if fix_scene_numbering:
result_fixed = copy.copy(final_result)
result_fixed = copy.deepcopy(final_result)
result_fixed.fix_scene_numbering()
return result_fixed

View file

@ -54,6 +54,8 @@ class TVEpisode(tv.TVEpisode):
self._name = name
self._season = season
self._episode = episode
self._scene_season = season
self._scene_episode = episode
self._airdate = datetime.date(2010, 3, 9)
self.show = TVShow()
self._status = Quality.compositeStatus(common.DOWNLOADED, common.Quality.SDTV)

View file

@ -258,7 +258,7 @@ class BTNProvider(generic.TorrentProvider):
else:
# Do a general name search for the episode, formatted like SXXEYY
search_params['name'] = "S%02dE%02d" % (ep_obj.season, ep_obj.episode)
search_params['name'] = "S%02dE%02d" % (ep_obj.scene_season, ep_obj.scene_episode)
to_return = [search_params]

View file

@ -63,7 +63,7 @@ class DTTProvider(generic.TorrentProvider):
return search_string
def _get_episode_search_strings(self, episode):
return self._get_season_search_strings(episode.show, episode.season)
return self._get_season_search_strings(episode.show, episode.scene_season)
def _doSearch(self, search_params, show=None, age=None):

View file

@ -95,8 +95,8 @@ class EZRSSProvider(generic.TorrentProvider):
if ep_obj.show.air_by_date:
params['date'] = str(ep_obj.airdate)
else:
params['season'] = ep_obj.season
params['episode'] = ep_obj.episode
params['season'] = ep_obj.scene_season
params['episode'] = ep_obj.scene_episode
return [params]

View file

@ -25,7 +25,7 @@ import sys
import re
import urllib
import urllib2
import copy
import itertools
import operator
import collections
@ -41,7 +41,6 @@ from sickbeard import encodingKludge as ek
from sickbeard.exceptions import ex
from lib.hachoir_parser import createParser
from sickbeard.name_parser.parser import NameParser, InvalidNameException
from sickbeard import scene_numbering
from sickbeard.common import Quality, Overview
@ -243,13 +242,8 @@ class GenericProvider:
self._checkAuth()
# XEM episode scene numbering
with episode.lock:
sceneEpisode = copy.deepcopy(episode)
sceneEpisode.convertToSceneNumbering()
logger.log(u'Searching "%s" for "%s" as "%s"'
% (self.name, episode.prettyName(), sceneEpisode.prettyName()))
% (self.name, episode.prettyName(), episode.scene_prettyName()))
self.cache.updateCache()
results = self.cache.searchCache(episode, manualSearch)
@ -264,7 +258,7 @@ class GenericProvider:
itemList = []
for cur_search_string in self._get_episode_search_strings(sceneEpisode):
for cur_search_string in self._get_episode_search_strings(episode):
itemList += self._doSearch(cur_search_string, show=episode.show)
for item in itemList:
@ -314,21 +308,20 @@ class GenericProvider:
itemList = []
results = {}
sceneSeasons = {}
seasons = {}
searchSeason = False
# convert wanted seasons and episodes to XEM scene numbering
seasonEp = show.getAllEpisodes(season)
wantedEp = [x for x in seasonEp if show.getOverview(x.status) in (Overview.WANTED, Overview.QUAL)]
map(lambda x: x.convertToSceneNumbering(), wantedEp)
for x in wantedEp: sceneSeasons.setdefault(x.season, []).append(x)
[seasons.setdefault(x.scene_season, []).append(x) for x in wantedEp]
if wantedEp == seasonEp and not show.air_by_date:
if wantedEp == seasonEp:
searchSeason = True
for sceneSeason, sceneEpisodes in sceneSeasons.iteritems():
for curString in self._get_season_search_strings(show, sceneSeason, sceneEpisodes, searchSeason):
for season, episodes in seasons.iteritems():
for curString in self._get_season_search_strings(show, season, episodes, searchSeason):
itemList += self._doSearch(curString)
for item in itemList:

View file

@ -153,8 +153,8 @@ class HDBitsProvider(generic.TorrentProvider):
if episode:
post_data['tvdb'] = {
'id': show.indexerid,
'season': episode.season,
'episode': episode.episode
'season': episode.scene_season,
'episode': episode.scene_episode
}
if season:

View file

@ -149,8 +149,8 @@ class HDTorrentsProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))
@ -181,8 +181,6 @@ class HDTorrentsProvider(generic.TorrentProvider):
if not data:
continue
# Remove HDTorrents NEW list
split_data = data.partition('<!-- Show New Torrents After Last Visit -->\n\n\n\n')
data = split_data[2]

View file

@ -130,8 +130,8 @@ class IPTorrentsProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + ' %s' % add_string
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + ' %s' % add_string
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))

View file

@ -208,12 +208,12 @@ class KATProvider(generic.TorrentProvider):
else:
for show_name in set(allPossibleShowNames(ep_obj.show)):
ep_string = sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + '|' + \
sickbeard.config.naming_ep_type[0] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + '|' + \
sickbeard.config.naming_ep_type[3] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + ' %s category:tv' % add_string
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + '|' + \
sickbeard.config.naming_ep_type[0] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + '|' + \
sickbeard.config.naming_ep_type[3] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + ' %s category:tv' % add_string
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))
return [search_string]

View file

@ -273,7 +273,7 @@ class NewzbinProvider(generic.NZBProvider):
nameList = set(show_name_helpers.allPossibleShowNames(ep_obj.show))
if not ep_obj.show.air_by_date:
searchStr = " OR ".join(['^"' + x + ' - %dx%02d"' % (ep_obj.season, ep_obj.episode) for x in nameList])
searchStr = " OR ".join(['^"' + x + ' - %dx%02d"' % (ep_obj.scene_season, ep_obj.scene_episode) for x in nameList])
else:
searchStr = " OR ".join(['^"' + x + ' - ' + str(ep_obj.airdate) + '"' for x in nameList])
return [searchStr]

View file

@ -127,8 +127,8 @@ class NewznabProvider(generic.NZBProvider):
params['season'] = date_str.partition('-')[0]
params['ep'] = date_str.partition('-')[2].replace('-', '/')
else:
params['season'] = ep_obj.season
params['ep'] = ep_obj.episode
params['season'] = ep_obj.scene_season
params['ep'] = ep_obj.scene_episode
to_return = [params]

View file

@ -167,8 +167,8 @@ class NextGenProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))

View file

@ -67,7 +67,7 @@ class NyaaProvider(generic.TorrentProvider):
return names
def _get_episode_search_strings(self, ep_obj):
return self._get_season_search_strings(ep_obj.show, ep_obj.season)
return self._get_season_search_strings(ep_obj.show, ep_obj.scene_season)
def _doSearch(self, search_string, show=None, age=None):
@ -136,8 +136,8 @@ class NyaaProvider(generic.TorrentProvider):
# parse the file name
try:
myParser = NameParser(show=episode.show)
parse_result = myParser.parse(title)
myParser = NameParser(False)
parse_result = myParser.parse(title, True)
except InvalidNameException:
logger.log(u"Unable to parse the filename " + title + " into a valid episode", logger.WARNING)
continue

View file

@ -115,8 +115,8 @@ class PublicHDProvider(generic.TorrentProvider):
else:
for show_name in set(allPossibleShowNames(ep_obj.show)):
ep_string = sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
for x in add_string.split('|'):
to_search = re.sub('\s+', ' ', ep_string + ' %s' % x)

View file

@ -138,8 +138,8 @@ class SCCProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))

View file

@ -214,12 +214,12 @@ class ThePirateBayProvider(generic.TorrentProvider):
else:
for show_name in set(allPossibleShowNames(ep_obj.show)):
ep_string = sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + '|' + \
sickbeard.config.naming_ep_type[0] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode} + '|' + \
sickbeard.config.naming_ep_type[3] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + '|' + \
sickbeard.config.naming_ep_type[0] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode} + '|' + \
sickbeard.config.naming_ep_type[3] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
ep_string += ' %s' % add_string

View file

@ -152,8 +152,8 @@ class TorrentDayProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))

View file

@ -133,8 +133,8 @@ class TorrentLeechProvider(generic.TorrentProvider):
else:
for show_name in set(show_name_helpers.allPossibleShowNames(ep_obj.show)):
ep_string = show_name_helpers.sanitizeSceneName(show_name) + ' ' + \
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.season,
'episodenumber': ep_obj.episode}
sickbeard.config.naming_ep_type[2] % {'seasonnumber': ep_obj.scene_season,
'episodenumber': ep_obj.scene_episode}
search_string['Episode'].append(re.sub('\s+', ' ', ep_string))

View file

@ -170,8 +170,8 @@ def makeSceneSearchString(episode):
if episode.show.air_by_date and episode.airdate != datetime.date.fromordinal(1):
epStrings = [str(episode.airdate)]
else:
epStrings = ["S%02iE%02i" % (int(episode.season), int(episode.episode)),
"%ix%02i" % (int(episode.season), int(episode.episode))]
epStrings = ["S%02iE%02i" % (int(episode.scene_season), int(episode.scene_episode)),
"%ix%02i" % (int(episode.scene_season), int(episode.scene_episode))]
# for single-season shows just search for the show name -- if total ep count (exclude s0) is less than 11
# due to the amount of qualities and releases, it is easy to go over the 50 result limit on rss feeds otherwise

View file

@ -1125,10 +1125,14 @@ def dirty_setter(attr_name):
class TVEpisode(object):
def __init__(self, show, season, episode, file=""):
# Convert season/episode to XEM scene numbering
scene_season, scene_episode = sickbeard.scene_numbering.get_scene_numbering(show.indexerid, season, episode)
self._name = ""
self._season = season
self._episode = episode
self._scene_season = scene_season
self._scene_episode = scene_episode
self._description = ""
self._subtitles = list()
self._subtitles_searchcount = 0
@ -1162,6 +1166,8 @@ class TVEpisode(object):
name = property(lambda self: self._name, dirty_setter("_name"))
season = property(lambda self: self._season, dirty_setter("_season"))
episode = property(lambda self: self._episode, dirty_setter("_episode"))
scene_season = property(lambda self: self._scene_season, dirty_setter("_scene_season"))
scene_episode = property(lambda self: self._scene_episode, dirty_setter("_scene_episode"))
description = property(lambda self: self._description, dirty_setter("_description"))
subtitles = property(lambda self: self._subtitles, dirty_setter("_subtitles"))
subtitles_searchcount = property(lambda self: self._subtitles_searchcount, dirty_setter("_subtitles_searchcount"))
@ -1728,6 +1734,16 @@ class TVEpisode(object):
return self._format_pattern('%SN - %Sx%0E - %EN')
def scene_prettyName(self):
"""
Returns the name of this episode in a "pretty" human-readable format. Used for logging
and notifications and such.
Returns: A string representing the episode's name and season/ep numbers
"""
return self._format_pattern('%SN - %SSx%0SE - %EN')
def _ep_name(self):
"""
Returns the name of the episode to use during renaming. Combines the names of related episodes.
@ -1827,6 +1843,10 @@ class TVEpisode(object):
'%0S': '%02d' % self.season,
'%E': str(self.episode),
'%0E': '%02d' % self.episode,
'%SS': str(self.scene_season),
'%0SS': '%02d' % self.scene_season,
'%SE': str(self.scene_episode),
'%0SE': '%02d' % self.scene_episode,
'%RN': release_name(self.release_name),
'%RG': release_group(self.release_name),
'%AD': str(self.airdate).replace('-', ' '),
@ -2105,25 +2125,3 @@ class TVEpisode(object):
self.saveToDB()
for relEp in self.relatedEps:
relEp.saveToDB()
def convertToSceneNumbering(self):
if self.show.air_by_date: return
if self.season is None: return # can't work without a season
if self.episode is None: return # need to know the episode
indexer_id = self.show.indexerid
(self.season, self.episode) = sickbeard.scene_numbering.get_scene_numbering(indexer_id, self.season,
self.episode)
def convertToIndexer(self):
if self.show.air_by_date: return
if self.season is None: return # can't work without a season
if self.episode is None: return # need to know the episode
indexer_id = self.show.indexerid
(self.season, self.episode) = sickbeard.scene_numbering.get_indexer_numbering(indexer_id, self.season,
self.episode)