Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Concept version of upgrading based on preferred words #5987

Open
wants to merge 14 commits into
base: develop
Choose a base branch
from
Open
123 changes: 120 additions & 3 deletions medusa/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@
# You should have received a copy of the GNU General Public License
# along with Medusa. If not, see <http://www.gnu.org/licenses/>.
"""Collection of generic used classes."""
from __future__ import unicode_literals
from __future__ import division, unicode_literals
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rtms: division can be removed again.


import logging

from dateutil import parser

from medusa import app
from medusa.common import Quality
from medusa import app, db
from medusa.common import statusStrings, Quality, UNSET, WANTED
from medusa.helper.common import episode_num
from medusa.logger.adapters.style import BraceAdapter
from medusa.search import SearchType

Expand Down Expand Up @@ -132,6 +133,122 @@ def __init__(self, episodes=None, provider=None):
# For example SearchType.MANUAL_SEARCH, SearchType.FORCED_SEARCH, SearchType.DAILY_SEARCH, SearchType.PROPER_SEARCH
self.search_type = None

# # Assign a score for the amount of preffered words there are in the release name
self._preferred_words_score = None

@property
def preferred_words_score(self):
"""Calculate a score based on the amount of preferred words in release name."""
preferred_words = [word.lower() for word in app.PREFERRED_WORDS]
self._preferred_words_score = round((len([word for word in preferred_words if word in self.name.lower()]) / float(len(preferred_words))) * 100)
return self._preferred_words_score

@staticmethod
def __qualities_to_string(qualities=None):
return ', '.join([Quality.qualityStrings[quality] for quality in qualities or []
if quality and quality in Quality.qualityStrings]) or 'None'

def want_episode(self, season, episode, quality):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@medariox i've moved want_episode from the Series class to Result class. Imo it looks allot cleaner, with less parameter. And it just feels like this is a better home.

"""Whether or not the episode with the specified quality is wanted.

:param season:
:type season: int
:param episode:
:type episode: int
:param quality:
:type quality: int

:return:
:rtype: bool
"""
# if the quality isn't one we want under any circumstances then just say no
allowed_qualities, preferred_qualities = self.series.current_qualities
log.debug(
u'{id}: Allowed, Preferred = [ {allowed} ] [ {preferred} ] Found = [ {found} ]', {
'id': self.series.series_id,
'allowed': self.__qualities_to_string(allowed_qualities),
'preferred': self.__qualities_to_string(preferred_qualities),
'found': self.__qualities_to_string([quality]),
}
)

if not Quality.wanted_quality(quality, allowed_qualities, preferred_qualities):
log.debug(
u"{id}: Ignoring found result for '{show}' {ep} with unwanted quality '{quality}'", {
'id': self.series.series_id,
'show': self.series.name,
'ep': self.series.episode_num(season, episode),
'quality': Quality.qualityStrings[quality],
}
)
return False

main_db_con = db.DBConnection()

sql_results = main_db_con.select(
'SELECT '
' status, quality, '
' manually_searched, preferred_words_score '
'FROM '
' tv_episodes '
'WHERE '
' indexer = ? '
' AND showid = ? '
' AND season = ? '
' AND episode = ?', [self.series.indexer, self.series.series_id, season, episode])

if not sql_results or not len(sql_results):
log.debug(
u'{id}: Unable to find a matching episode in database.'
u' Ignoring found result for {show} {ep} with quality {quality}', {
'id': self.series.series_id,
'show': self.series.name,
'ep': episode_num(season, episode),
'quality': Quality.qualityStrings[quality],
}
)
return False

cur_status, cur_quality, preferred_words_score = (
int(sql_results[0]['status'] or UNSET),
int(sql_results[0]['quality'] or Quality.NA),
int(sql_results[0]['preferred_words_score'] or 0)
)

ep_status_text = statusStrings[cur_status]
manually_searched = sql_results[0]['manually_searched']

# if it's one of these then we want it as long as it's in our allowed initial qualities
if cur_status == WANTED:
should_replace, reason = (
True, u"Current status is 'WANTED'. Accepting result with quality '{new_quality}'".format(
new_quality=Quality.qualityStrings[quality]
)
)
else:
upgrade_preferred_words = all([self.series.upgrade_preferred_words,
self.preferred_words_score,
self.preferred_words_score < self.series.preferred_words_score])
should_replace, reason = Quality.should_replace(cur_status, cur_quality, quality, allowed_qualities,
preferred_qualities, self.download_current_quality,
self.forced_search, manually_searched, self.search_type,
upgrade_preferred_words=upgrade_preferred_words)

log.debug(
u"{id}: '{show}' {ep} status is: '{status}'."
u" {action} result with quality '{new_quality}'."
u' Reason: {reason}', {
'id': self.series.series_id,
'show': self.name,
'ep': episode_num(season, episode),
'status': ep_status_text,
'action': 'Accepting' if should_replace else 'Ignoring',
'new_quality': Quality.qualityStrings[quality],
'reason': reason,
}
)
return should_replace

@property
def actual_episode(self):
return self._actual_episode
Expand Down
8 changes: 7 additions & 1 deletion medusa/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,8 @@ def should_search(cur_status, cur_quality, show_obj, manually_searched):

@staticmethod
def should_replace(ep_status, old_quality, new_quality, allowed_qualities, preferred_qualities,
download_current_quality=False, force=False, manually_searched=False, search_type=None):
download_current_quality=False, force=False, manually_searched=False, search_type=None,
upgrade_preferred_words=None):
"""Return true if the old quality should be replaced with new quality.

If not preferred qualities, then any downloaded quality is final
Expand Down Expand Up @@ -588,6 +589,8 @@ def should_replace(ep_status, old_quality, new_quality, allowed_qualities, prefe
if preferred_qualities:
# Don't replace because old quality is already best quality.
if old_quality in preferred_qualities:
if upgrade_preferred_words:
return True, 'Old quality is already best, but we got a preferred words upgrade'
return False, 'Existing quality is already a preferred quality. Ignoring new quality'

# Replace if preferred quality
Expand All @@ -600,6 +603,9 @@ def should_replace(ep_status, old_quality, new_quality, allowed_qualities, prefe
return False, 'New quality is same/lower quality (and not preferred). Ignoring new quality'

else:
# Allowed quality can only be replaced in case of a upgrade based on preferred words
if upgrade_preferred_words:
return True, 'Old quality is already best, but we got a preferred words upgrade'
# Allowed quality should never be replaced
return False, 'Existing quality is already final (allowed only). Ignoring new quality'

Expand Down
23 changes: 23 additions & 0 deletions medusa/databases/main_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,3 +882,26 @@ def execute(self):
self.addColumn('tv_shows', 'airdate_offset', 'NUMERIC', 0)

self.inc_minor_version()


class AddPreferredWordsUpgradeFields(AddTvshowStartSearchOffset):
"""Add tv_show / tv_episodes preferred words upgrade fields."""

def test(self):
"""Test if the version is at least 44.14"""
return self.connection.version >= (44, 14)

def execute(self):
utils.backup_database(self.connection.path, self.connection.version)

log.info(u'Adding new preferred words upgrade field in the tv_shows table')
if not self.hasColumn('tv_shows', 'upgrade_preferred_words'):
self.addColumn('tv_shows', 'upgrade_preferred_words', 'NUMERIC', 0)
if not self.hasColumn('tv_shows', 'preferred_words_score'):
self.addColumn('tv_shows', 'preferred_words_score', 'NUMERIC', 0)

log.info(u'Adding new preferred words upgrade field in the tv_episodes table')
if not self.hasColumn('tv_episodes', 'preferred_words_score'):
self.addColumn('tv_episodes', 'preferred_words_score', 'NUMERIC', 0)

self.inc_minor_version()
64 changes: 31 additions & 33 deletions medusa/search/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,48 +185,50 @@ def snatch_episode(result):
# don't notify when we re-download an episode
sql_l = []
trakt_data = []
for curEpObj in result.episodes:
with curEpObj.lock:
for cur_ep_obj in result.episodes:
with cur_ep_obj.lock:
if is_first_best_match(result):
curEpObj.status = SNATCHED_BEST
curEpObj.quality = result.quality
cur_ep_obj.status = SNATCHED_BEST
cur_ep_obj.quality = result.quality
else:
curEpObj.status = end_status
curEpObj.quality = result.quality
cur_ep_obj.status = end_status
cur_ep_obj.quality = result.quality
# Reset all others fields to the snatched status
# New snatch by default doesn't have nfo/tbn
curEpObj.hasnfo = False
curEpObj.hastbn = False
cur_ep_obj.hasnfo = False
cur_ep_obj.hastbn = False

# We can't reset location because we need to know what we are replacing
# curEpObj.location = ''
# cur_ep_obj.location = ''

# Release name and group are parsed in PP
curEpObj.release_name = ''
curEpObj.release_group = ''
cur_ep_obj.release_name = ''
cur_ep_obj.release_group = ''

# Need to reset subtitle settings because it's a different file
curEpObj.subtitles = list()
curEpObj.subtitles_searchcount = 0
curEpObj.subtitles_lastsearch = u'0001-01-01 00:00:00'
cur_ep_obj.subtitles = list()
cur_ep_obj.subtitles_searchcount = 0
cur_ep_obj.subtitles_lastsearch = u'0001-01-01 00:00:00'

# Need to store the correct is_proper. Not use the old one
curEpObj.is_proper = is_proper
curEpObj.version = 0
cur_ep_obj.is_proper = is_proper
cur_ep_obj.version = 0

curEpObj.manually_searched = result.manually_searched
cur_ep_obj.manually_searched = result.manually_searched

sql_l.append(curEpObj.get_sql())
cur_ep_obj.preferred_words_score = result.preferred_words_score

if curEpObj.status != common.DOWNLOADED:
notifiers.notify_snatch(curEpObj, result)
sql_l.append(cur_ep_obj.get_sql())

if cur_ep_obj.status != common.DOWNLOADED:
notifiers.notify_snatch(cur_ep_obj, result)

if app.USE_TRAKT and app.TRAKT_SYNC_WATCHLIST:
trakt_data.append((curEpObj.season, curEpObj.episode))
trakt_data.append((cur_ep_obj.season, cur_ep_obj.episode))
log.info(
u'Adding {0} {1} to Trakt watchlist',
result.series.name,
episode_num(curEpObj.season, curEpObj.episode),
episode_num(cur_ep_obj.season, cur_ep_obj.episode),
)

if trakt_data:
Expand Down Expand Up @@ -274,9 +276,7 @@ def filter_results(results):
if cur_result.actual_episodes:
wanted_ep = False
for episode in cur_result.actual_episodes:
if series_obj.want_episode(cur_result.actual_season, episode, cur_result.quality,
cur_result.forced_search, cur_result.download_current_quality,
search_type=cur_result.search_type):
if cur_result.want_episode(cur_result.actual_season, episode, cur_result.quality):
wanted_ep = True

if not wanted_ep:
Expand Down Expand Up @@ -707,8 +707,7 @@ def search_providers(series_obj, episodes, forced_search=False, down_cur_quality
# From our provider multi_episode and single_episode results, collect candidates.
cache_found_results = list_results_for_provider(cache_search_results, found_results, cur_provider)
# We're passing the empty lists, because we don't want to include previous candidates
cache_multi, cache_single = collect_candidates(cache_found_results, cur_provider, [],
[], series_obj, down_cur_quality)
cache_multi, cache_single = collect_candidates(cache_found_results, cur_provider, [], [])

# For now we only search if we didn't get any results back from cache,
# but we might wanna check if there was something useful in cache.
Expand Down Expand Up @@ -761,8 +760,7 @@ def search_providers(series_obj, episodes, forced_search=False, down_cur_quality
# Continue because we don't want to pick best results as we are running a manual search by user
continue

multi_results, single_results = collect_candidates(found_results, cur_provider, multi_results,
single_results, series_obj, down_cur_quality)
multi_results, single_results = collect_candidates(found_results, cur_provider, multi_results, single_results)

# Remove provider from thread name before return results
threading.currentThread().name = original_thread_name
Expand All @@ -774,13 +772,13 @@ def search_providers(series_obj, episodes, forced_search=False, down_cur_quality
return combine_results(multi_results, single_results)


def collect_candidates(found_results, provider, multi_results, single_results, series_obj, down_cur_quality):
def collect_candidates(found_results, provider, multi_results, single_results):
"""Collect candidates for episode, multi-episode or season results."""
candidates = (candidate for result, candidate in iteritems(found_results[provider.name])
if result in (SEASON_RESULT, MULTI_EP_RESULT))
candidates = list(itertools.chain(*candidates))
if candidates:
multi_results += collect_multi_candidates(candidates, series_obj, down_cur_quality)
multi_results += collect_multi_candidates(candidates)

# Collect candidates for single-episode results
single_results = collect_single_candidates(found_results[provider.name], single_results)
Expand Down Expand Up @@ -847,7 +845,7 @@ def collect_single_candidates(candidates, results):
return single_candidates + new_candidates


def collect_multi_candidates(candidates, series_obj, down_cur_quality):
def collect_multi_candidates(candidates):
"""Collect mutli-episode and season result candidates."""
multi_candidates = []

Expand All @@ -857,7 +855,7 @@ def collect_multi_candidates(candidates, series_obj, down_cur_quality):

for candidate in wanted_candidates:
wanted_episodes = [
series_obj.want_episode(ep_obj.season, ep_obj.episode, candidate.quality, down_cur_quality)
candidate.want_episode(ep_obj.season, ep_obj.episode, candidate.quality)
for ep_obj in candidate.episodes
]

Expand Down
2 changes: 2 additions & 0 deletions medusa/server/api/v2/series.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ def http_patch(self, series_slug, path_param=None):
'config.qualities.preferred': ListField(series, 'qualities_preferred'),
'config.qualities.combined': IntegerField(series, 'quality'),
'config.airdateOffset': IntegerField(series, 'airdate_offset'),
'config.upgradePreferredWords': BooleanField(series, 'upgrade_preferred_words'),
'config.preferredWordsScore': IntegerField(series, 'preferred_words_score'),
}

for key, value in iter_nested_items(data):
Expand Down
3 changes: 1 addition & 2 deletions medusa/tv/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -486,8 +486,7 @@ def find_needed_episodes(self, episodes, forced_search=False, down_cur_quality=F
all_wanted = True
for cur_ep in search_result.actual_episodes:
# if the show says we want that episode then add it to the list
if not search_result.series.want_episode(search_result.actual_season, cur_ep, search_result.quality,
forced_search, down_cur_quality):
if not search_result.want_episode(search_result.actual_season, cur_ep, search_result.quality):
log.debug('Ignoring {0} because one or more episodes are unwanted', search_result.name)
all_wanted = False
break
Expand Down